iobroker.time-tracker 0.2.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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/admin/i18n/de.json +34 -0
  4. package/admin/i18n/en.json +1 -0
  5. package/admin/i18n/es.json +34 -0
  6. package/admin/i18n/fr.json +34 -0
  7. package/admin/i18n/it.json +34 -0
  8. package/admin/i18n/nl.json +34 -0
  9. package/admin/i18n/pl.json +34 -0
  10. package/admin/i18n/pt.json +34 -0
  11. package/admin/i18n/ru.json +34 -0
  12. package/admin/i18n/uk.json +34 -0
  13. package/admin/i18n/zh-cn.json +34 -0
  14. package/admin/jsonConfig.json +266 -0
  15. package/admin/time-tracker.png +0 -0
  16. package/build/lib/adapter/automation.js +100 -0
  17. package/build/lib/adapter/automation.js.map +7 -0
  18. package/build/lib/adapter/commands.js +160 -0
  19. package/build/lib/adapter/commands.js.map +7 -0
  20. package/build/lib/adapter/messages.js +207 -0
  21. package/build/lib/adapter/messages.js.map +7 -0
  22. package/build/lib/adapter/presence.js +129 -0
  23. package/build/lib/adapter/presence.js.map +7 -0
  24. package/build/lib/adapter/states.js +330 -0
  25. package/build/lib/adapter/states.js.map +7 -0
  26. package/build/lib/adapter/triggers.js +92 -0
  27. package/build/lib/adapter/triggers.js.map +7 -0
  28. package/build/lib/db/database.js +96 -0
  29. package/build/lib/db/database.js.map +7 -0
  30. package/build/lib/db/migrations.js +603 -0
  31. package/build/lib/db/migrations.js.map +7 -0
  32. package/build/lib/db/repositories/absences.js +432 -0
  33. package/build/lib/db/repositories/absences.js.map +7 -0
  34. package/build/lib/db/repositories/audit.js +90 -0
  35. package/build/lib/db/repositories/audit.js.map +7 -0
  36. package/build/lib/db/repositories/automations.js +326 -0
  37. package/build/lib/db/repositories/automations.js.map +7 -0
  38. package/build/lib/db/repositories/entries.js +335 -0
  39. package/build/lib/db/repositories/entries.js.map +7 -0
  40. package/build/lib/db/repositories/holidays.js +156 -0
  41. package/build/lib/db/repositories/holidays.js.map +7 -0
  42. package/build/lib/db/repositories/payouts.js +221 -0
  43. package/build/lib/db/repositories/payouts.js.map +7 -0
  44. package/build/lib/db/repositories/rfid.js +264 -0
  45. package/build/lib/db/repositories/rfid.js.map +7 -0
  46. package/build/lib/db/repositories/rules.js +356 -0
  47. package/build/lib/db/repositories/rules.js.map +7 -0
  48. package/build/lib/db/repositories/settings.js +91 -0
  49. package/build/lib/db/repositories/settings.js.map +7 -0
  50. package/build/lib/db/repositories/terminals.js +243 -0
  51. package/build/lib/db/repositories/terminals.js.map +7 -0
  52. package/build/lib/db/repositories/triggers.js +240 -0
  53. package/build/lib/db/repositories/triggers.js.map +7 -0
  54. package/build/lib/db/repositories/users.js +491 -0
  55. package/build/lib/db/repositories/users.js.map +7 -0
  56. package/build/lib/db/seed.js +180 -0
  57. package/build/lib/db/seed.js.map +7 -0
  58. package/build/lib/domain/avatar.js +63 -0
  59. package/build/lib/domain/avatar.js.map +7 -0
  60. package/build/lib/domain/breaks.js +70 -0
  61. package/build/lib/domain/breaks.js.map +7 -0
  62. package/build/lib/domain/calculation.js +150 -0
  63. package/build/lib/domain/calculation.js.map +7 -0
  64. package/build/lib/domain/holidays.js +113 -0
  65. package/build/lib/domain/holidays.js.map +7 -0
  66. package/build/lib/domain/punch.js +99 -0
  67. package/build/lib/domain/punch.js.map +7 -0
  68. package/build/lib/domain/target.js +89 -0
  69. package/build/lib/domain/target.js.map +7 -0
  70. package/build/lib/errors.js +56 -0
  71. package/build/lib/errors.js.map +7 -0
  72. package/build/lib/reports/labels.js +358 -0
  73. package/build/lib/reports/labels.js.map +7 -0
  74. package/build/lib/reports/pdf.js +308 -0
  75. package/build/lib/reports/pdf.js.map +7 -0
  76. package/build/lib/reports/types.js +32 -0
  77. package/build/lib/reports/types.js.map +7 -0
  78. package/build/lib/reports/xls.js +211 -0
  79. package/build/lib/reports/xls.js.map +7 -0
  80. package/build/lib/services/aggregation.js +495 -0
  81. package/build/lib/services/aggregation.js.map +7 -0
  82. package/build/lib/services/auth.js +381 -0
  83. package/build/lib/services/auth.js.map +7 -0
  84. package/build/lib/services/backup.js +372 -0
  85. package/build/lib/services/backup.js.map +7 -0
  86. package/build/lib/services/closing.js +106 -0
  87. package/build/lib/services/closing.js.map +7 -0
  88. package/build/lib/services/sessionSecret.js +100 -0
  89. package/build/lib/services/sessionSecret.js.map +7 -0
  90. package/build/lib/services/sync.js +278 -0
  91. package/build/lib/services/sync.js.map +7 -0
  92. package/build/lib/util/time.js +125 -0
  93. package/build/lib/util/time.js.map +7 -0
  94. package/build/lib/web/api.js +2589 -0
  95. package/build/lib/web/api.js.map +7 -0
  96. package/build/lib/web/cookies.js +80 -0
  97. package/build/lib/web/cookies.js.map +7 -0
  98. package/build/lib/web/events.js +50 -0
  99. package/build/lib/web/events.js.map +7 -0
  100. package/build/lib/web/pin-guard.js +86 -0
  101. package/build/lib/web/pin-guard.js.map +7 -0
  102. package/build/lib/web/problem.js +129 -0
  103. package/build/lib/web/problem.js.map +7 -0
  104. package/build/lib/web/rate-limit.js +65 -0
  105. package/build/lib/web/rate-limit.js.map +7 -0
  106. package/build/lib/web/router.js +274 -0
  107. package/build/lib/web/router.js.map +7 -0
  108. package/build/lib/web/server.js +189 -0
  109. package/build/lib/web/server.js.map +7 -0
  110. package/build/lib/web/static.js +151 -0
  111. package/build/lib/web/static.js.map +7 -0
  112. package/build/lib/web/stream.js +135 -0
  113. package/build/lib/web/stream.js.map +7 -0
  114. package/build/main.js +986 -0
  115. package/build/main.js.map +7 -0
  116. package/io-package.json +221 -0
  117. package/package.json +131 -0
  118. package/src-pwa/src/i18n/de.json +359 -0
  119. package/src-pwa/src/i18n/en.json +359 -0
  120. package/src-pwa/src/i18n/es.json +359 -0
  121. package/src-pwa/src/i18n/fr.json +359 -0
  122. package/src-pwa/src/i18n/index.ts +96 -0
  123. package/src-pwa/src/i18n/it.json +359 -0
  124. package/src-pwa/src/i18n/nl.json +359 -0
  125. package/src-pwa/src/i18n/pl.json +359 -0
  126. package/src-pwa/src/i18n/pt.json +359 -0
  127. package/src-pwa/src/i18n/ru.json +359 -0
  128. package/src-pwa/src/i18n/uk.json +359 -0
  129. package/src-pwa/src/i18n/zh-cn.json +359 -0
  130. package/www/assets/index-QI3wrEvW.js +119 -0
  131. package/www/assets/index-QI3wrEvW.js.map +1 -0
  132. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
  133. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
  134. package/www/favicon.svg +16 -0
  135. package/www/icon-192.png +0 -0
  136. package/www/icon-512.png +0 -0
  137. package/www/index.html +38 -0
  138. package/www/manifest.webmanifest +1 -0
  139. package/www/person.png +0 -0
  140. package/www/sw.js +3 -0
  141. package/www/sw.js.map +1 -0
  142. package/www/workbox-9c191d2f.js +3 -0
  143. package/www/workbox-9c191d2f.js.map +1 -0
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var router_exports = {};
20
+ __export(router_exports, {
21
+ DEFAULT_MAX_BODY_BYTES: () => DEFAULT_MAX_BODY_BYTES,
22
+ binary: () => binary,
23
+ createRouter: () => createRouter,
24
+ isRawResponse: () => isRawResponse,
25
+ json: () => json,
26
+ noContent: () => noContent
27
+ });
28
+ module.exports = __toCommonJS(router_exports);
29
+ var import_cookies = require("./cookies");
30
+ var import_problem = require("./problem");
31
+ var import_rate_limit = require("./rate-limit");
32
+ const ROUTE_RESPONSE = Symbol("routeResponse");
33
+ const RAW_RESPONSE = Symbol("rawResponse");
34
+ function json(status, body, headers = {}) {
35
+ return Object.assign({ status, body, headers }, { [ROUTE_RESPONSE]: true });
36
+ }
37
+ function noContent(status = 204, headers = {}) {
38
+ return Object.assign({ status, body: void 0, headers }, { [ROUTE_RESPONSE]: true });
39
+ }
40
+ function binary(status, body, contentType, headers = {}) {
41
+ return Object.assign(
42
+ { status, body, headers: { "content-type": contentType, ...headers } },
43
+ { [ROUTE_RESPONSE]: true, [RAW_RESPONSE]: true }
44
+ );
45
+ }
46
+ function isRawResponse(value) {
47
+ return typeof value === "object" && value !== null && RAW_RESPONSE in value;
48
+ }
49
+ function isRouteResponse(value) {
50
+ return typeof value === "object" && value !== null && ROUTE_RESPONSE in value;
51
+ }
52
+ const BASE_HEADERS = {
53
+ "content-type": "application/json; charset=utf-8",
54
+ "cache-control": "no-store",
55
+ "x-content-type-options": "nosniff",
56
+ "referrer-policy": "no-referrer"
57
+ };
58
+ const PROBLEM_HEADERS = {
59
+ ...BASE_HEADERS,
60
+ "content-type": import_problem.PROBLEM_CONTENT_TYPE
61
+ };
62
+ const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
63
+ function splitPath(path) {
64
+ return path.split("/").filter((segment) => segment.length > 0);
65
+ }
66
+ function compileSegments(path) {
67
+ return splitPath(path).map(
68
+ (segment) => segment.startsWith(":") ? { literal: null, name: segment.slice(1) } : { literal: segment, name: "" }
69
+ );
70
+ }
71
+ function problemResponse(status, code, detail, instance, headers = {}) {
72
+ const details = new import_problem.HttpProblem(status, code, detail).toProblem(instance);
73
+ return {
74
+ status,
75
+ headers: { ...PROBLEM_HEADERS, ...headers },
76
+ body: JSON.stringify(details)
77
+ };
78
+ }
79
+ function forwardedAddress(headerValue) {
80
+ const raw = headerValue("x-forwarded-for");
81
+ if (!raw) {
82
+ return null;
83
+ }
84
+ const hops = raw.split(",").map((hop) => hop.trim()).filter((hop) => hop.length > 0);
85
+ return hops.length > 0 ? hops[hops.length - 1] : null;
86
+ }
87
+ function resolveClientAddress(request, headerValue, trustProxy) {
88
+ var _a;
89
+ const forwarded = trustProxy ? forwardedAddress(headerValue) : null;
90
+ return (_a = forwarded != null ? forwarded : request.remoteAddress) != null ? _a : null;
91
+ }
92
+ function forwardedProto(headerValue) {
93
+ var _a, _b;
94
+ return ((_b = ((_a = headerValue("x-forwarded-proto")) != null ? _a : "").split(",")[0]) != null ? _b : "").trim().toLowerCase();
95
+ }
96
+ const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
97
+ function createRouter(options) {
98
+ var _a, _b;
99
+ const routes = [];
100
+ const maxBodyBytes = (_a = options.maxBodyBytes) != null ? _a : DEFAULT_MAX_BODY_BYTES;
101
+ const now = (_b = options.now) != null ? _b : (() => Math.floor(Date.now() / 1e3));
102
+ const limiter = (0, import_rate_limit.createRateLimiter)();
103
+ return {
104
+ add(route) {
105
+ routes.push({
106
+ definition: route,
107
+ methods: Array.isArray(route.method) ? route.method : [route.method],
108
+ segments: compileSegments(route.path)
109
+ });
110
+ },
111
+ routes() {
112
+ return routes.flatMap((route) => route.methods.map((method) => ({ method, path: route.definition.path })));
113
+ },
114
+ async handle(request) {
115
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k;
116
+ const method = ((_a2 = request.method) != null ? _a2 : "GET").toUpperCase();
117
+ const path = (_b2 = request.path) != null ? _b2 : "/";
118
+ const pathSegments = splitPath(path);
119
+ const headers = (_c = request.headers) != null ? _c : {};
120
+ const headerValue = (name) => {
121
+ var _a3, _b3;
122
+ const value = (_a3 = headers[name]) != null ? _a3 : headers[name.toLowerCase()];
123
+ return Array.isArray(value) ? (_b3 = value[0]) != null ? _b3 : null : value != null ? value : null;
124
+ };
125
+ const clientAddress = resolveClientAddress(request, headerValue, options.trustProxy === true);
126
+ const secure = options.trustProxy === true && forwardedProto(headerValue) === "https";
127
+ const routedRequest = ((_d = request.remoteAddress) != null ? _d : null) === clientAddress ? request : { ...request, remoteAddress: clientAddress };
128
+ const matched = [];
129
+ for (const route2 of routes) {
130
+ if (route2.segments.length !== pathSegments.length) {
131
+ continue;
132
+ }
133
+ const params2 = {};
134
+ let matches = true;
135
+ for (let index = 0; index < route2.segments.length; index++) {
136
+ const segment = route2.segments[index];
137
+ if (segment.literal === null) {
138
+ params2[segment.name] = decodeURIComponent(pathSegments[index]);
139
+ continue;
140
+ }
141
+ if (segment.literal !== pathSegments[index]) {
142
+ matches = false;
143
+ break;
144
+ }
145
+ }
146
+ if (matches) {
147
+ matched.push({ route: route2, params: params2 });
148
+ }
149
+ }
150
+ if (matched.length === 0) {
151
+ return problemResponse(404, "not_found", `no route for ${method} ${path}`, path);
152
+ }
153
+ const candidate = matched.find((entry) => entry.route.methods.includes(method));
154
+ if (!candidate) {
155
+ const allowed = [...new Set(matched.flatMap((entry) => entry.route.methods))].sort();
156
+ return problemResponse(405, "method_not_allowed", `allowed: ${allowed.join(", ")}`, path, {
157
+ allow: allowed.join(", ")
158
+ });
159
+ }
160
+ const { route, params } = candidate;
161
+ const body = (_e = request.body) != null ? _e : "";
162
+ const bodyLimit = (_f = route.definition.maxBodyBytes) != null ? _f : maxBodyBytes;
163
+ const bodySize = typeof body === "string" ? Buffer.byteLength(body) : body.length;
164
+ if (bodySize > bodyLimit) {
165
+ return problemResponse(413, "payload_too_large", `body exceeds ${bodyLimit} bytes`, path);
166
+ }
167
+ const bearerToken = (_g = headerValue("x-session-token")) != null ? _g : "";
168
+ const cookieToken = (_h = (0, import_cookies.parseCookies)(headerValue("cookie"))[import_cookies.SESSION_COOKIE]) != null ? _h : "";
169
+ const token = bearerToken || cookieToken;
170
+ const viaCookie = bearerToken === "" && cookieToken !== "";
171
+ const requiresCsrf = (_i = route.definition.requiresCsrf) != null ? _i : !SAFE_METHODS.includes(method) && cookieToken !== "";
172
+ if (route.definition.rateLimit) {
173
+ const limited = limiter.check(route.definition.rateLimit, clientAddress != null ? clientAddress : "unknown", now());
174
+ if (!limited.allowed) {
175
+ return problemResponse(429, "rate_limited", "too many requests, try again later", path, {
176
+ "retry-after": String(limited.retryAfterSeconds),
177
+ "x-ratelimit-remaining": "0"
178
+ });
179
+ }
180
+ }
181
+ let auth = null;
182
+ let rotated = null;
183
+ if (route.definition.requiresAuth !== false) {
184
+ const result = options.auth.authenticate({
185
+ token,
186
+ permission: route.definition.permission,
187
+ now: now()
188
+ });
189
+ if (!result.ok) {
190
+ const status = result.error === "permission_denied" ? 403 : 401;
191
+ return problemResponse(status, result.error, `request rejected (${result.error})`, path);
192
+ }
193
+ auth = result.context;
194
+ if (requiresCsrf && !options.auth.verifyCsrf({ token, csrfToken: (_j = headerValue("x-csrf-token")) != null ? _j : "" })) {
195
+ return problemResponse(403, "csrf_rejected", "missing or wrong CSRF token", path);
196
+ }
197
+ rotated = cookieToken !== "" ? options.auth.rotateIfDue({ token, now: now() }) : null;
198
+ }
199
+ function parseRequestBody(required) {
200
+ var _a3;
201
+ const contentType = (_a3 = headerValue("content-type")) != null ? _a3 : "";
202
+ if (contentType && !contentType.toLowerCase().includes("application/json")) {
203
+ throw new import_problem.HttpProblem(415, "unsupported_media_type", `unsupported content type ${contentType}`);
204
+ }
205
+ const text = Buffer.isBuffer(body) ? body.toString("utf8") : body;
206
+ if (!text.trim()) {
207
+ if (required) {
208
+ throw new import_problem.HttpProblem(400, "bad_request", "body is required");
209
+ }
210
+ return {};
211
+ }
212
+ try {
213
+ return JSON.parse(text);
214
+ } catch {
215
+ throw new import_problem.HttpProblem(400, "bad_request", "body is not valid JSON");
216
+ }
217
+ }
218
+ const context = {
219
+ request: routedRequest,
220
+ params,
221
+ auth,
222
+ sessionToken: token,
223
+ viaCookie,
224
+ secure,
225
+ query: (name) => {
226
+ var _a3, _b3;
227
+ const value = (_a3 = request.query) == null ? void 0 : _a3[name];
228
+ return Array.isArray(value) ? (_b3 = value[0]) != null ? _b3 : null : value != null ? value : null;
229
+ },
230
+ header: headerValue,
231
+ jsonBody: () => parseRequestBody(true),
232
+ optionalJsonBody: () => parseRequestBody(false),
233
+ rawBody: () => {
234
+ var _a3;
235
+ return Buffer.isBuffer(request.body) ? request.body : Buffer.from((_a3 = request.body) != null ? _a3 : "", "utf8");
236
+ }
237
+ };
238
+ try {
239
+ const result = await route.definition.handler(context);
240
+ if (!isRouteResponse(result)) {
241
+ throw new Error("handler did not return a response object");
242
+ }
243
+ const responseHeaders = { ...BASE_HEADERS, ...(_k = result.headers) != null ? _k : {} };
244
+ if (rotated && !responseHeaders["set-cookie"]) {
245
+ responseHeaders["set-cookie"] = (0, import_cookies.serializeCookie)(import_cookies.SESSION_COOKIE, rotated.token, {
246
+ maxAgeSeconds: Math.max(0, rotated.expiresAt - now()),
247
+ secure
248
+ });
249
+ }
250
+ if (result.body === void 0) {
251
+ delete responseHeaders["content-type"];
252
+ return { status: result.status, headers: responseHeaders, body: "" };
253
+ }
254
+ if (isRawResponse(result)) {
255
+ return { status: result.status, headers: responseHeaders, body: result.body };
256
+ }
257
+ return { status: result.status, headers: responseHeaders, body: JSON.stringify(result.body) };
258
+ } catch (error) {
259
+ const { problem: details } = (0, import_problem.toProblem)(error, path);
260
+ return { status: details.status, headers: { ...PROBLEM_HEADERS }, body: JSON.stringify(details) };
261
+ }
262
+ }
263
+ };
264
+ }
265
+ // Annotate the CommonJS export names for ESM import in node:
266
+ 0 && (module.exports = {
267
+ DEFAULT_MAX_BODY_BYTES,
268
+ binary,
269
+ createRouter,
270
+ isRawResponse,
271
+ json,
272
+ noContent
273
+ });
274
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/web/router.ts"],
4
+ "sourcesContent": ["/**\n * Server agnostic HTTP router.\n *\n * The router only sees plain request/response objects, so the same routes can later be mounted on an own HTTP\n * server (`native.port`) or be handed to the web extension of a `web` instance. Everything security relevant\n * happens here and not in the single handlers: session check, permission check (RBAC), CSRF token for state\n * changing requests and a body size limit.\n *\n * Handlers return `json(status, body)` or `noContent()`; any thrown error is converted by `toProblem`.\n */\n\nimport type { AuthContext, AuthService } from \"../services/auth\";\nimport { parseCookies, SESSION_COOKIE, serializeCookie } from \"./cookies\";\nimport { HttpProblem, PROBLEM_CONTENT_TYPE, toProblem, type ProblemCode, type ProblemDetails } from \"./problem\";\nimport { createRateLimiter } from \"./rate-limit\";\n\n/** Supported HTTP methods. */\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\n/** A request as the router sees it (the body is already read and size limited). */\nexport interface HttpRequest {\n\t/** HTTP method, upper case */\n\tmethod: string;\n\t/** Path without query string, e.g. `/reports/day` */\n\tpath: string;\n\t/** Query parameters, repeated names become arrays */\n\tquery?: Record<string, string | string[] | undefined>;\n\t/** Request headers, names lower case */\n\theaders?: Record<string, string | string[] | undefined>;\n\t/** Raw body: JSON text for the JSON endpoints, bytes for an upload */\n\tbody?: string | Buffer;\n\t/** Client address */\n\tremoteAddress?: string | null;\n}\n\n/** A response as the router produces it. */\nexport interface HttpResponse {\n\t/** HTTP status code */\n\tstatus: number;\n\t/** Response headers (lower case names) */\n\theaders: Record<string, string>;\n\t/** Response body, already serialised (a Buffer for binary content) */\n\tbody: string | Buffer;\n}\n\n/** Result of a handler. */\nexport interface RouteResponse {\n\t/** HTTP status code */\n\tstatus: number;\n\t/** Body, serialised as JSON when it is not `undefined` */\n\tbody?: unknown;\n\t/** Additional headers */\n\theaders?: Record<string, string>;\n}\n\n/** What a handler receives. */\nexport interface RouteContext {\n\t/** The incoming request */\n\trequest: HttpRequest;\n\t/** Path parameters of the route */\n\tparams: Record<string, string>;\n\t/** Authenticated context, `null` for public routes */\n\tauth: AuthContext | null;\n\t/** Session token of the request: the bearer header or, for browsers, the session cookie */\n\tsessionToken: string;\n\t/** True when no bearer header was sent and the session came from the cookie */\n\tviaCookie: boolean;\n\t/** True when the client talks HTTPS (a trusted proxy reported `x-forwarded-proto: https`) */\n\tsecure: boolean;\n\t/** Reads a query parameter */\n\tquery(name: string): string | null;\n\t/** Reads a request header */\n\theader(name: string): string | null;\n\t/** Parses the JSON body, throws `400 bad_request` when it is not usable */\n\tjsonBody<T = Record<string, unknown>>(): T;\n\t/** Parses the JSON body; an empty body becomes an empty object (optional fields) */\n\toptionalJsonBody<T = Record<string, unknown>>(): T;\n\t/** The body as bytes \u2014 binary uploads arrive here, the JSON helpers are for everything else */\n\trawBody(): Buffer;\n}\n\n/** A single route. */\nexport interface RouteDefinition {\n\t/** Method (or methods) the route answers */\n\tmethod: HttpMethod | HttpMethod[];\n\t/** Path pattern, `:name` marks a parameter, e.g. `/reports/day/:date` */\n\tpath: string;\n\t/** Session required (default true) */\n\trequiresAuth?: boolean;\n\t/** Permission the caller has to hold */\n\tpermission?: string;\n\t/** CSRF token required (default true for state changing methods) */\n\trequiresCsrf?: boolean;\n\t/** Rate limit of the route class (counted per client address) */\n\trateLimit?: { name: string; limit: number; windowSeconds: number };\n\t/**\n\t * Body limit of this route in bytes; the router-wide limit applies when it is missing.\n\t *\n\t * A backup upload is bigger than any other request the API accepts, so that one route raises the limit while\n\t * everything else stays where it was.\n\t */\n\tmaxBodyBytes?: number;\n\t/** Handler of the route */\n\thandler: (context: RouteContext) => RouteResponse | Promise<RouteResponse>;\n}\n\n/** Options of the router. */\nexport interface RouterOptions {\n\t/** Authentication service used for the session check */\n\tauth: AuthService;\n\t/** Maximum body size in bytes (default 2 MiB) */\n\tmaxBodyBytes?: number;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n\t/**\n\t * Trusts the `x-forwarded-*` headers of a reverse proxy. Only switch this on when a proxy is really in\n\t * front: the declared client address lands in the audit trail and decides the rate limit buckets, so a\n\t * client that is allowed to set the header itself could shift both.\n\t */\n\ttrustProxy?: boolean;\n}\n\n/** The router. */\nexport interface Router {\n\t/** Registers a route */\n\tadd(route: RouteDefinition): void;\n\t/** Handles a request; never throws */\n\thandle(request: HttpRequest): Promise<HttpResponse>;\n\t/** Registered routes (method and path), e.g. for the API documentation */\n\troutes(): { method: string; path: string }[];\n}\n\n/** Marker to tell a handler result from a plain JSON payload. */\nconst ROUTE_RESPONSE = Symbol(\"routeResponse\");\n\n/** Marker for responses whose body is passed to the transport unchanged. */\nconst RAW_RESPONSE = Symbol(\"rawResponse\");\n\n/** A response whose body is written to the transport as it is (see `binary`). */\nexport interface RawRouteResponse extends RouteResponse {\n\t/** Bytes or text */\n\tbody: string | Buffer;\n}\n\n/**\n * Creates a response object for a handler.\n *\n * @param status - HTTP status code\n * @param body - body to serialise as JSON\n * @param headers - additional response headers\n * @returns route response\n */\nexport function json(status: number, body: unknown, headers: Record<string, string> = {}): RouteResponse {\n\treturn Object.assign({ status, body, headers }, { [ROUTE_RESPONSE]: true });\n}\n\n/**\n * Creates an empty response.\n *\n * @param status - HTTP status code (default 204)\n * @param headers - additional response headers (e.g. a `set-cookie` that ends a session)\n * @returns route response without a body\n */\nexport function noContent(status = 204, headers: Record<string, string> = {}): RouteResponse {\n\treturn Object.assign({ status, body: undefined, headers }, { [ROUTE_RESPONSE]: true });\n}\n\n/**\n * Creates a response with a body that is not JSON.\n *\n * Used for downloads and other binary payloads (reports, backups). The body is handed to the transport\n * unchanged, so a `Buffer` keeps its bytes and a string is written as UTF-8 text.\n *\n * @param status - HTTP status code\n * @param body - bytes or text\n * @param contentType - media type of the body\n * @param headers - additional response headers (e.g. `content-disposition`)\n * @returns route response\n */\nexport function binary(\n\tstatus: number,\n\tbody: string | Buffer,\n\tcontentType: string,\n\theaders: Record<string, string> = {},\n): RouteResponse {\n\treturn Object.assign(\n\t\t{ status, body, headers: { \"content-type\": contentType, ...headers } },\n\t\t{ [ROUTE_RESPONSE]: true, [RAW_RESPONSE]: true },\n\t);\n}\n\n/**\n * Checks whether a response body is handed to the transport unchanged.\n *\n * @param value - handler result\n * @returns true for responses created by `binary`\n */\nexport function isRawResponse(value: unknown): value is RawRouteResponse {\n\treturn typeof value === \"object\" && value !== null && RAW_RESPONSE in value;\n}\n\n/**\n * Checks whether a handler result is a route response.\n *\n * @param value - handler result\n * @returns true when the value was created by `json` or `noContent`\n */\nfunction isRouteResponse(value: unknown): value is RouteResponse {\n\treturn typeof value === \"object\" && value !== null && ROUTE_RESPONSE in value;\n}\n\n/** Headers every API response carries. */\nconst BASE_HEADERS: Record<string, string> = {\n\t\"content-type\": \"application/json; charset=utf-8\",\n\t\"cache-control\": \"no-store\",\n\t\"x-content-type-options\": \"nosniff\",\n\t\"referrer-policy\": \"no-referrer\",\n};\n\n/** Headers of a problem document: the same security headers with the RFC 9457 content type. */\nconst PROBLEM_HEADERS: Record<string, string> = {\n\t...BASE_HEADERS,\n\t\"content-type\": PROBLEM_CONTENT_TYPE,\n};\n\n/** State changing methods need a CSRF token by default. */\nconst SAFE_METHODS: string[] = [\"GET\", \"HEAD\", \"OPTIONS\"];\n\ninterface CompiledRoute {\n\tdefinition: RouteDefinition;\n\tmethods: HttpMethod[];\n\t/** Path segments, `literal: null` marks a parameter */\n\tsegments: { literal: string | null; name: string }[];\n}\n\n/**\n * Splits a path into segments without leading/trailing slashes.\n *\n * @param path - path to split\n * @returns segments\n */\nfunction splitPath(path: string): string[] {\n\treturn path.split(\"/\").filter(segment => segment.length > 0);\n}\n\n/**\n * Compiles the path pattern of a route.\n *\n * @param path - pattern like `/reports/day/:date`\n * @returns segments with their parameter names\n */\nfunction compileSegments(path: string): { literal: string | null; name: string }[] {\n\treturn splitPath(path).map(segment =>\n\t\tsegment.startsWith(\":\") ? { literal: null, name: segment.slice(1) } : { literal: segment, name: \"\" },\n\t);\n}\n\n/**\n * Creates a problem response.\n *\n * @param status - HTTP status code\n * @param code - stable error code\n * @param detail - optional explanation\n * @param instance - request path\n * @param headers - additional headers\n * @returns HTTP response\n */\nfunction problemResponse(\n\tstatus: number,\n\tcode: ProblemCode,\n\tdetail: string | undefined,\n\tinstance: string,\n\theaders: Record<string, string> = {},\n): HttpResponse {\n\tconst details: ProblemDetails = new HttpProblem(status, code, detail).toProblem(instance);\n\treturn {\n\t\tstatus,\n\t\theaders: { ...PROBLEM_HEADERS, ...headers },\n\t\tbody: JSON.stringify(details),\n\t};\n}\n\n/** Reads a request header (lower case names). */\ntype HeaderReader = (name: string) => string | null;\n\n/**\n * Reads the last hop of `x-forwarded-for`.\n *\n * Every client may send the header itself; the proxy *appends* the address it saw, so the rightmost entry is\n * the one the trusted proxy wrote. Taking the first one instead would let a caller pick its own address \u2014 and\n * with it the rate limit bucket and the audit entry.\n *\n * @param headerValue - header reader\n * @returns the forwarded client address or `null` when the header is missing or empty\n */\nfunction forwardedAddress(headerValue: HeaderReader): string | null {\n\tconst raw = headerValue(\"x-forwarded-for\");\n\tif (!raw) {\n\t\treturn null;\n\t}\n\tconst hops = raw\n\t\t.split(\",\")\n\t\t.map(hop => hop.trim())\n\t\t.filter(hop => hop.length > 0);\n\treturn hops.length > 0 ? hops[hops.length - 1] : null;\n}\n\n/**\n * Resolves the client address used for rate limits and the audit trail.\n *\n * @param request - incoming request\n * @param headerValue - header reader\n * @param trustProxy - true when the operator declared a reverse proxy in front\n * @returns the client address\n */\nfunction resolveClientAddress(request: HttpRequest, headerValue: HeaderReader, trustProxy: boolean): string | null {\n\tconst forwarded = trustProxy ? forwardedAddress(headerValue) : null;\n\treturn forwarded ?? request.remoteAddress ?? null;\n}\n\n/**\n * Reads the protocol a trusted proxy reported.\n *\n * @param headerValue - header reader\n * @returns `https` when the request reached the proxy over TLS, otherwise the raw value\n */\nfunction forwardedProto(headerValue: HeaderReader): string {\n\treturn ((headerValue(\"x-forwarded-proto\") ?? \"\").split(\",\")[0] ?? \"\").trim().toLowerCase();\n}\n\n/**\n * Default limit of a request body.\n *\n * It has to be roomier than the biggest value a client may store: a branding picture is a data URL of up to\n * `MAX_BRANDING_BYTES` (512 KiB), and base64 makes it about a third bigger. Everything else the API accepts\n * (a punch, a patch, a report request) is a few kilobytes.\n */\nexport const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;\n\n/**\n * Creates the router.\n *\n * @param options - authentication service and limits\n * @returns router instance\n */\nexport function createRouter(options: RouterOptions): Router {\n\tconst routes: CompiledRoute[] = [];\n\tconst maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\tconst now = options.now ?? (() => Math.floor(Date.now() / 1000));\n\t// the counters live in the router, so the limits belong to the API instance that registered them\n\tconst limiter = createRateLimiter();\n\n\treturn {\n\t\tadd(route: RouteDefinition): void {\n\t\t\troutes.push({\n\t\t\t\tdefinition: route,\n\t\t\t\tmethods: Array.isArray(route.method) ? route.method : [route.method],\n\t\t\t\tsegments: compileSegments(route.path),\n\t\t\t});\n\t\t},\n\n\t\troutes(): { method: string; path: string }[] {\n\t\t\treturn routes.flatMap(route => route.methods.map(method => ({ method, path: route.definition.path })));\n\t\t},\n\n\t\tasync handle(request: HttpRequest): Promise<HttpResponse> {\n\t\t\tconst method = (request.method ?? \"GET\").toUpperCase();\n\t\t\tconst path = request.path ?? \"/\";\n\t\t\tconst pathSegments = splitPath(path);\n\t\t\tconst headers = request.headers ?? {};\n\t\t\tconst headerValue = (name: string): string | null => {\n\t\t\t\tconst value = headers[name] ?? headers[name.toLowerCase()];\n\t\t\t\treturn Array.isArray(value) ? (value[0] ?? null) : (value ?? null);\n\t\t\t};\n\n\t\t\t// Behind a reverse proxy the socket address is the proxy itself, so the address the proxy appended to\n\t\t\t// `x-forwarded-for` is used \u2014 but only when the operator declared the proxy as trusted (`trustProxy`),\n\t\t\t// because any client can send that header on its own. One hop is assumed, which is what a single\n\t\t\t// nginx/caddy in front produces.\n\t\t\tconst clientAddress = resolveClientAddress(request, headerValue, options.trustProxy === true);\n\t\t\tconst secure = options.trustProxy === true && forwardedProto(headerValue) === \"https\";\n\t\t\t// the handlers see the address the rate limit counted and the audit trail stores\n\t\t\tconst routedRequest: HttpRequest =\n\t\t\t\t(request.remoteAddress ?? null) === clientAddress\n\t\t\t\t\t? request\n\t\t\t\t\t: { ...request, remoteAddress: clientAddress };\n\n\t\t\tconst matched: { route: CompiledRoute; params: Record<string, string> }[] = [];\n\t\t\tfor (const route of routes) {\n\t\t\t\tif (route.segments.length !== pathSegments.length) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst params: Record<string, string> = {};\n\t\t\t\tlet matches = true;\n\t\t\t\tfor (let index = 0; index < route.segments.length; index++) {\n\t\t\t\t\tconst segment = route.segments[index];\n\t\t\t\t\tif (segment.literal === null) {\n\t\t\t\t\t\tparams[segment.name] = decodeURIComponent(pathSegments[index]);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (segment.literal !== pathSegments[index]) {\n\t\t\t\t\t\tmatches = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (matches) {\n\t\t\t\t\tmatched.push({ route, params });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matched.length === 0) {\n\t\t\t\treturn problemResponse(404, \"not_found\", `no route for ${method} ${path}`, path);\n\t\t\t}\n\n\t\t\tconst candidate = matched.find(entry => entry.route.methods.includes(method as HttpMethod));\n\t\t\tif (!candidate) {\n\t\t\t\tconst allowed = [...new Set(matched.flatMap(entry => entry.route.methods))].sort();\n\t\t\t\treturn problemResponse(405, \"method_not_allowed\", `allowed: ${allowed.join(\", \")}`, path, {\n\t\t\t\t\tallow: allowed.join(\", \"),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst { route, params } = candidate;\n\t\t\tconst body = request.body ?? \"\";\n\t\t\t// a route may raise the limit for itself (a backup upload is bigger than any other body)\n\t\t\tconst bodyLimit = route.definition.maxBodyBytes ?? maxBodyBytes;\n\t\t\tconst bodySize = typeof body === \"string\" ? Buffer.byteLength(body) : body.length;\n\t\t\tif (bodySize > bodyLimit) {\n\t\t\t\treturn problemResponse(413, \"payload_too_large\", `body exceeds ${bodyLimit} bytes`, path);\n\t\t\t}\n\n\t\t\t// The session travels in the `x-session-token` header (integration clients) or in the httpOnly\n\t\t\t// session cookie (browsers, specification 4.x). CSRF is only possible where the browser attaches a\n\t\t\t// credential by itself, so a state changing request needs the CSRF token as soon as the cookie is\n\t\t\t// present \u2014 while a pure bearer client, which no foreign page can equip with a header, needs none.\n\t\t\tconst bearerToken = headerValue(\"x-session-token\") ?? \"\";\n\t\t\tconst cookieToken = parseCookies(headerValue(\"cookie\"))[SESSION_COOKIE] ?? \"\";\n\t\t\tconst token = bearerToken || cookieToken;\n\t\t\tconst viaCookie = bearerToken === \"\" && cookieToken !== \"\";\n\t\t\tconst requiresCsrf =\n\t\t\t\troute.definition.requiresCsrf ?? (!SAFE_METHODS.includes(method) && cookieToken !== \"\");\n\n\t\t\t// the limit is counted per client address, before any work is done\n\t\t\tif (route.definition.rateLimit) {\n\t\t\t\tconst limited = limiter.check(route.definition.rateLimit, clientAddress ?? \"unknown\", now());\n\t\t\t\tif (!limited.allowed) {\n\t\t\t\t\treturn problemResponse(429, \"rate_limited\", \"too many requests, try again later\", path, {\n\t\t\t\t\t\t\"retry-after\": String(limited.retryAfterSeconds),\n\t\t\t\t\t\t\"x-ratelimit-remaining\": \"0\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet auth: AuthContext | null = null;\n\t\t\tlet rotated: { token: string; expiresAt: number } | null = null;\n\n\t\t\tif (route.definition.requiresAuth !== false) {\n\t\t\t\tconst result = options.auth.authenticate({\n\t\t\t\t\ttoken,\n\t\t\t\t\tpermission: route.definition.permission,\n\t\t\t\t\tnow: now(),\n\t\t\t\t});\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\tconst status = result.error === \"permission_denied\" ? 403 : 401;\n\t\t\t\t\treturn problemResponse(status, result.error, `request rejected (${result.error})`, path);\n\t\t\t\t}\n\t\t\t\tauth = result.context;\n\n\t\t\t\tif (requiresCsrf && !options.auth.verifyCsrf({ token, csrfToken: headerValue(\"x-csrf-token\") ?? \"\" })) {\n\t\t\t\t\treturn problemResponse(403, \"csrf_rejected\", \"missing or wrong CSRF token\", path);\n\t\t\t\t}\n\n\t\t\t\t// A browser session that has been in use for a while gets a fresh token: the cookie is replaced\n\t\t\t\t// below and the old token dies with its row (specification 4.10: rotation). Bearer clients keep\n\t\t\t\t// their token \u2014 they hold no cookie that could be renewed without asking them.\n\t\t\t\trotated = cookieToken !== \"\" ? options.auth.rotateIfDue({ token, now: now() }) : null;\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Parses the request body.\n\t\t\t *\n\t\t\t * @param required - true when an empty body is a client error\n\t\t\t * @returns the parsed body\n\t\t\t */\n\t\t\tfunction parseRequestBody<T>(required: boolean): T {\n\t\t\t\tconst contentType = headerValue(\"content-type\") ?? \"\";\n\t\t\t\tif (contentType && !contentType.toLowerCase().includes(\"application/json\")) {\n\t\t\t\t\tthrow new HttpProblem(415, \"unsupported_media_type\", `unsupported content type ${contentType}`);\n\t\t\t\t}\n\t\t\t\t// a body that arrived as bytes (an upload) cannot be JSON; the text endpoints send text\n\t\t\t\tconst text = Buffer.isBuffer(body) ? body.toString(\"utf8\") : body;\n\t\t\t\tif (!text.trim()) {\n\t\t\t\t\tif (required) {\n\t\t\t\t\t\tthrow new HttpProblem(400, \"bad_request\", \"body is required\");\n\t\t\t\t\t}\n\t\t\t\t\t// DELETE requests often carry no body at all; the optional fields stay empty then\n\t\t\t\t\treturn {} as T;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\treturn JSON.parse(text) as T;\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new HttpProblem(400, \"bad_request\", \"body is not valid JSON\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst context: RouteContext = {\n\t\t\t\trequest: routedRequest,\n\t\t\t\tparams,\n\t\t\t\tauth,\n\t\t\t\tsessionToken: token,\n\t\t\t\tviaCookie,\n\t\t\t\tsecure,\n\t\t\t\tquery: name => {\n\t\t\t\t\tconst value = request.query?.[name];\n\t\t\t\t\treturn Array.isArray(value) ? (value[0] ?? null) : (value ?? null);\n\t\t\t\t},\n\t\t\t\theader: headerValue,\n\t\t\t\tjsonBody: <T = Record<string, unknown>>(): T => parseRequestBody<T>(true),\n\t\t\t\toptionalJsonBody: <T = Record<string, unknown>>(): T => parseRequestBody<T>(false),\n\t\t\t\trawBody: (): Buffer =>\n\t\t\t\t\tBuffer.isBuffer(request.body) ? request.body : Buffer.from(request.body ?? \"\", \"utf8\"),\n\t\t\t};\n\n\t\t\ttry {\n\t\t\t\tconst result = await route.definition.handler(context);\n\t\t\t\tif (!isRouteResponse(result)) {\n\t\t\t\t\tthrow new Error(\"handler did not return a response object\");\n\t\t\t\t}\n\n\t\t\t\tconst responseHeaders = { ...BASE_HEADERS, ...(result.headers ?? {}) };\n\t\t\t\tif (rotated && !responseHeaders[\"set-cookie\"]) {\n\t\t\t\t\t// the renewed session cookie of a browser; routes that set their own (login, logout) win\n\t\t\t\t\tresponseHeaders[\"set-cookie\"] = serializeCookie(SESSION_COOKIE, rotated.token, {\n\t\t\t\t\t\tmaxAgeSeconds: Math.max(0, rotated.expiresAt - now()),\n\t\t\t\t\t\tsecure,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (result.body === undefined) {\n\t\t\t\t\tdelete responseHeaders[\"content-type\"];\n\t\t\t\t\treturn { status: result.status, headers: responseHeaders, body: \"\" };\n\t\t\t\t}\n\t\t\t\tif (isRawResponse(result)) {\n\t\t\t\t\t// downloads keep their bytes: a buffer is written as is, a string as UTF-8 text\n\t\t\t\t\treturn { status: result.status, headers: responseHeaders, body: result.body };\n\t\t\t\t}\n\t\t\t\treturn { status: result.status, headers: responseHeaders, body: JSON.stringify(result.body) };\n\t\t\t} catch (error) {\n\t\t\t\tconst { problem: details } = toProblem(error, path);\n\t\t\t\treturn { status: details.status, headers: { ...PROBLEM_HEADERS }, body: JSON.stringify(details) };\n\t\t\t}\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b", "route", "params"]
7
+ }
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var server_exports = {};
30
+ __export(server_exports, {
31
+ normalizeBindAddress: () => normalizeBindAddress,
32
+ startWebServer: () => startWebServer
33
+ });
34
+ module.exports = __toCommonJS(server_exports);
35
+ var http = __toESM(require("node:http"));
36
+ var import_problem = require("./problem");
37
+ var import_router = require("./router");
38
+ var import_stream = require("./stream");
39
+ function normalizePrefix(prefix) {
40
+ const trimmed = prefix.trim();
41
+ if (trimmed === "" || trimmed === "/") {
42
+ return "";
43
+ }
44
+ return trimmed.startsWith("/") ? trimmed.replace(/\/+$/, "") : `/${trimmed.replace(/\/+$/, "")}`;
45
+ }
46
+ async function readBody(request, maxBytes) {
47
+ var _a;
48
+ const chunks = [];
49
+ let size = 0;
50
+ for await (const chunk of request) {
51
+ const buffer = chunk;
52
+ size += buffer.length;
53
+ if (size > maxBytes) {
54
+ return null;
55
+ }
56
+ chunks.push(buffer);
57
+ }
58
+ const body = Buffer.concat(chunks);
59
+ return ((_a = request.headers["content-type"]) != null ? _a : "").toLowerCase().startsWith("application/octet-stream") ? body : body.toString("utf8");
60
+ }
61
+ function parseQuery(requestUrl) {
62
+ var _a;
63
+ const url = new URL(requestUrl, "http://localhost");
64
+ const query = {};
65
+ for (const key of new Set(url.searchParams.keys())) {
66
+ const values = url.searchParams.getAll(key);
67
+ query[key] = values.length > 1 ? values : (_a = values[0]) != null ? _a : "";
68
+ }
69
+ return query;
70
+ }
71
+ function normalizeBindAddress(value) {
72
+ const raw = (value != null ? value : "").trim();
73
+ if (raw === "") {
74
+ return "127.0.0.1";
75
+ }
76
+ if (raw === "*") {
77
+ return "0.0.0.0";
78
+ }
79
+ const bracketed = /^\[([^\]]+)]:\d+$/.exec(raw);
80
+ if (bracketed) {
81
+ return bracketed[1];
82
+ }
83
+ const withPort = /^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/.exec(raw);
84
+ if (withPort) {
85
+ return withPort[1];
86
+ }
87
+ return raw;
88
+ }
89
+ async function startWebServer(options) {
90
+ var _a, _b, _c, _d, _e;
91
+ const maxBodyBytes = (_a = options.maxBodyBytes) != null ? _a : import_router.DEFAULT_MAX_BODY_BYTES;
92
+ const bind = normalizeBindAddress(options.bind);
93
+ const apiPrefix = normalizePrefix((_b = options.apiPrefix) != null ? _b : "/api");
94
+ const handle = async (request, response) => {
95
+ var _a2, _b2, _c2, _d2, _e2, _f, _g, _h, _i, _j;
96
+ try {
97
+ const body = await readBody(request, maxBodyBytes);
98
+ if (body === null) {
99
+ const details = new import_problem.HttpProblem(413, "payload_too_large", `body exceeds ${maxBodyBytes} bytes`);
100
+ response.writeHead(413, { "content-type": import_problem.PROBLEM_CONTENT_TYPE });
101
+ response.end(JSON.stringify(details.toProblem((_a2 = request.url) != null ? _a2 : "/")));
102
+ return;
103
+ }
104
+ const url = new URL((_b2 = request.url) != null ? _b2 : "/", "http://localhost");
105
+ const headers = request.headers;
106
+ const remoteAddress = (_c2 = request.socket.remoteAddress) != null ? _c2 : null;
107
+ if (apiPrefix !== "" && url.pathname.startsWith(`${apiPrefix}/`)) {
108
+ const routerRequest = {
109
+ method: (_d2 = request.method) != null ? _d2 : "GET",
110
+ path: url.pathname.slice(apiPrefix.length),
111
+ query: parseQuery((_e2 = request.url) != null ? _e2 : "/"),
112
+ headers,
113
+ body,
114
+ remoteAddress
115
+ };
116
+ const result = await options.router.handle(routerRequest);
117
+ response.writeHead(result.status, result.headers);
118
+ response.end(result.body);
119
+ return;
120
+ }
121
+ const staticRequest = {
122
+ method: (_f = request.method) != null ? _f : "GET",
123
+ path: url.pathname,
124
+ query: parseQuery((_g = request.url) != null ? _g : "/"),
125
+ headers,
126
+ body,
127
+ remoteAddress
128
+ };
129
+ const file = (_i = (_h = options.staticFiles) == null ? void 0 : _h.call(options, staticRequest)) != null ? _i : null;
130
+ if (file) {
131
+ response.writeHead(file.status, file.headers);
132
+ response.end(file.body);
133
+ return;
134
+ }
135
+ response.writeHead(404, { "content-type": import_problem.PROBLEM_CONTENT_TYPE });
136
+ response.end(
137
+ JSON.stringify(
138
+ new import_problem.HttpProblem(404, "not_found", `no route for ${url.pathname}`).toProblem(url.pathname)
139
+ )
140
+ );
141
+ } catch (error) {
142
+ (_j = options.log) == null ? void 0 : _j.error(`request failed: ${error instanceof Error ? error.message : String(error)}`);
143
+ if (!response.headersSent) {
144
+ response.writeHead(500, { "content-type": import_problem.PROBLEM_CONTENT_TYPE });
145
+ }
146
+ response.end();
147
+ }
148
+ };
149
+ const server = http.createServer((request, response) => {
150
+ void handle(request, response);
151
+ });
152
+ server.on("clientError", (_error, socket) => socket.destroy());
153
+ await new Promise((resolve, reject) => {
154
+ const onError = (error) => reject(error);
155
+ server.once("error", onError);
156
+ server.listen(options.port, bind, () => {
157
+ server.off("error", onError);
158
+ resolve();
159
+ });
160
+ });
161
+ const address = server.address();
162
+ const port = (_c = address == null ? void 0 : address.port) != null ? _c : options.port;
163
+ const stream = options.stream ? (0, import_stream.attachEventStream)({
164
+ ...options.stream,
165
+ server,
166
+ path: (_d = options.stream.path) != null ? _d : `${apiPrefix}/stream`
167
+ }) : void 0;
168
+ (_e = options.log) == null ? void 0 : _e.info(
169
+ `API listening on http://${bind}:${port}${apiPrefix}${options.staticFiles ? " - web interface is served from disk" : ""}${stream ? " - live events on /stream" : ""}`
170
+ );
171
+ return {
172
+ port,
173
+ url: `http://${bind === "0.0.0.0" ? "127.0.0.1" : bind}:${port}`,
174
+ stream,
175
+ close: async () => {
176
+ await (stream == null ? void 0 : stream.close());
177
+ await new Promise((resolve) => {
178
+ server.close(() => resolve());
179
+ server.closeAllConnections();
180
+ });
181
+ }
182
+ };
183
+ }
184
+ // Annotate the CommonJS export names for ESM import in node:
185
+ 0 && (module.exports = {
186
+ normalizeBindAddress,
187
+ startWebServer
188
+ });
189
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/web/server.ts"],
4
+ "sourcesContent": ["/**\n * HTTP server that mounts the router on a Node http server.\n *\n * This is the adapter's own port (`native.port`): the PWA, the REST API and the kiosk terminal are served on\n * it. The server only translates between Node's request/response objects and the router \u2014 all routing,\n * authentication, RBAC and error handling live in the router and the API.\n *\n * Safety nets: the body is limited while it is being read (a client cannot fill the memory), the bind address\n * defaults to the local machine and a failure to bind never stops the adapter (the API is reported as\n * unavailable instead).\n */\n\nimport * as http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport { HttpProblem, PROBLEM_CONTENT_TYPE } from \"./problem\";\nimport { DEFAULT_MAX_BODY_BYTES, type HttpRequest, type Router } from \"./router\";\nimport type { StaticHandler } from \"./static\";\nimport { attachEventStream, type EventStream, type EventStreamOptions } from \"./stream\";\n\n/** Minimal logger used by the server. */\nexport interface ServerLogger {\n\t/** Informational message */\n\tinfo(message: string): void;\n\t/** Warning */\n\twarn(message: string): void;\n\t/** Error */\n\terror(message: string): void;\n}\n\n/** Options of the HTTP server. */\nexport interface WebServerOptions {\n\t/** Router that handles the requests */\n\trouter: Router;\n\t/** Port to listen on (`0` picks a free port) */\n\tport: number;\n\t/** Bind address, default `127.0.0.1` */\n\tbind?: string;\n\t/** Path prefix the API is mounted on (default `/api`) */\n\tapiPrefix?: string;\n\t/** Handler for the files of the web interface (PWA) */\n\tstaticFiles?: StaticHandler;\n\t/** Live event stream (`/api/stream`); without it the endpoint does not exist */\n\tstream?: Omit<EventStreamOptions, \"server\">;\n\t/** Maximum body size in bytes (default 2 MiB) */\n\tmaxBodyBytes?: number;\n\t/** Logger */\n\tlog?: ServerLogger;\n}\n\n/** A running server. */\nexport interface WebServer {\n\t/** Port the server is listening on */\n\treadonly port: number;\n\t/** Base URL of the server, e.g. `http://127.0.0.1:8092` */\n\treadonly url: string;\n\t/** Live event stream, when one is configured */\n\treadonly stream?: EventStream;\n\t/** Stops the server and closes all connections */\n\tclose(): Promise<void>;\n}\n\n/**\n * Normalises a path prefix.\n *\n * @param prefix - configured prefix\n * @returns prefix without trailing slash (`\"\"` disables the prefix)\n */\nfunction normalizePrefix(prefix: string): string {\n\tconst trimmed = prefix.trim();\n\tif (trimmed === \"\" || trimmed === \"/\") {\n\t\treturn \"\";\n\t}\n\treturn trimmed.startsWith(\"/\") ? trimmed.replace(/\\/+$/, \"\") : `/${trimmed.replace(/\\/+$/, \"\")}`;\n}\n\n/**\n * Reads the request body up to a limit.\n *\n * A body that is not text is kept as bytes: an uploaded backup has to arrive byte for byte. Everything else is\n * decoded here, because the endpoints expect a JSON document.\n *\n * @param request - incoming request\n * @param maxBytes - maximum number of bytes\n * @returns body as text or bytes, `null` when the limit is exceeded\n */\nasync function readBody(request: http.IncomingMessage, maxBytes: number): Promise<string | Buffer | null> {\n\tconst chunks: Buffer[] = [];\n\tlet size = 0;\n\n\tfor await (const chunk of request) {\n\t\tconst buffer = chunk as Buffer;\n\t\tsize += buffer.length;\n\t\tif (size > maxBytes) {\n\t\t\treturn null;\n\t\t}\n\t\tchunks.push(buffer);\n\t}\n\n\tconst body = Buffer.concat(chunks);\n\t// the raw upload arrives as bytes; a JSON document is decoded right away\n\treturn (request.headers[\"content-type\"] ?? \"\").toLowerCase().startsWith(\"application/octet-stream\")\n\t\t? body\n\t\t: body.toString(\"utf8\");\n}\n\n/**\n * Builds the query map of a request, repeated names become arrays.\n *\n * @param requestUrl - raw request URL\n * @returns query parameters\n */\nfunction parseQuery(requestUrl: string): Record<string, string | string[]> {\n\tconst url = new URL(requestUrl, \"http://localhost\");\n\tconst query: Record<string, string | string[]> = {};\n\tfor (const key of new Set(url.searchParams.keys())) {\n\t\tconst values = url.searchParams.getAll(key);\n\t\tquery[key] = values.length > 1 ? values : (values[0] ?? \"\");\n\t}\n\treturn query;\n}\n\n/**\n * Normalises the bind address of the instance settings.\n *\n * The admin offers the local addresses of the host in a list, and a value may come back in the `host:port` form\n * (`[::1]:8092` for IPv6) or as a wildcard. `listen` expects the address alone and the port comes from the\n * instance, so such a value cannot make the server fail to start. An empty value means \"this machine only\".\n *\n * @param value - raw value of the instance settings\n * @returns the address to bind to\n */\nexport function normalizeBindAddress(value: string | null | undefined): string {\n\tconst raw = (value ?? \"\").trim();\n\tif (raw === \"\") {\n\t\treturn \"127.0.0.1\";\n\t}\n\tif (raw === \"*\") {\n\t\treturn \"0.0.0.0\";\n\t}\n\t// [::1]:8092 or 192.168.1.5:8092 - the port belongs to the instance, not to the address\n\tconst bracketed = /^\\[([^\\]]+)]:\\d+$/.exec(raw);\n\tif (bracketed) {\n\t\treturn bracketed[1];\n\t}\n\tconst withPort = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(raw);\n\tif (withPort) {\n\t\treturn withPort[1];\n\t}\n\treturn raw;\n}\n\n/**\n * Starts the HTTP server.\n *\n * @param options - router, port and limits\n * @returns the running server\n */\nexport async function startWebServer(options: WebServerOptions): Promise<WebServer> {\n\tconst maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\tconst bind = normalizeBindAddress(options.bind);\n\tconst apiPrefix = normalizePrefix(options.apiPrefix ?? \"/api\");\n\n\tconst handle = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {\n\t\ttry {\n\t\t\tconst body = await readBody(request, maxBodyBytes);\n\t\t\tif (body === null) {\n\t\t\t\tconst details = new HttpProblem(413, \"payload_too_large\", `body exceeds ${maxBodyBytes} bytes`);\n\t\t\t\tresponse.writeHead(413, { \"content-type\": PROBLEM_CONTENT_TYPE });\n\t\t\t\tresponse.end(JSON.stringify(details.toProblem(request.url ?? \"/\")));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst url = new URL(request.url ?? \"/\", \"http://localhost\");\n\t\t\tconst headers = request.headers;\n\t\t\tconst remoteAddress = request.socket.remoteAddress ?? null;\n\n\t\t\t// the API lives below the prefix; everything else is a file of the web interface\n\t\t\tif (apiPrefix !== \"\" && url.pathname.startsWith(`${apiPrefix}/`)) {\n\t\t\t\tconst routerRequest: HttpRequest = {\n\t\t\t\t\tmethod: request.method ?? \"GET\",\n\t\t\t\t\tpath: url.pathname.slice(apiPrefix.length),\n\t\t\t\t\tquery: parseQuery(request.url ?? \"/\"),\n\t\t\t\t\theaders,\n\t\t\t\t\tbody,\n\t\t\t\t\tremoteAddress,\n\t\t\t\t};\n\t\t\t\tconst result = await options.router.handle(routerRequest);\n\t\t\t\tresponse.writeHead(result.status, result.headers);\n\t\t\t\tresponse.end(result.body);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst staticRequest: HttpRequest = {\n\t\t\t\tmethod: request.method ?? \"GET\",\n\t\t\t\tpath: url.pathname,\n\t\t\t\tquery: parseQuery(request.url ?? \"/\"),\n\t\t\t\theaders,\n\t\t\t\tbody,\n\t\t\t\tremoteAddress,\n\t\t\t};\n\t\t\tconst file = options.staticFiles?.(staticRequest) ?? null;\n\t\t\tif (file) {\n\t\t\t\tresponse.writeHead(file.status, file.headers);\n\t\t\t\tresponse.end(file.body);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tresponse.writeHead(404, { \"content-type\": PROBLEM_CONTENT_TYPE });\n\t\t\tresponse.end(\n\t\t\t\tJSON.stringify(\n\t\t\t\t\tnew HttpProblem(404, \"not_found\", `no route for ${url.pathname}`).toProblem(url.pathname),\n\t\t\t\t),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// the router never throws, so this is an unexpected failure of the transport itself\n\t\t\toptions.log?.error(`request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\tif (!response.headersSent) {\n\t\t\t\tresponse.writeHead(500, { \"content-type\": PROBLEM_CONTENT_TYPE });\n\t\t\t}\n\t\t\tresponse.end();\n\t\t}\n\t};\n\n\tconst server = http.createServer((request, response) => {\n\t\tvoid handle(request, response);\n\t});\n\tserver.on(\"clientError\", (_error, socket) => socket.destroy());\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst onError = (error: Error): void => reject(error);\n\t\tserver.once(\"error\", onError);\n\t\tserver.listen(options.port, bind, () => {\n\t\t\tserver.off(\"error\", onError);\n\t\t\tresolve();\n\t\t});\n\t});\n\n\tconst address = server.address() as AddressInfo | null;\n\tconst port = address?.port ?? options.port;\n\tconst stream = options.stream\n\t\t? attachEventStream({\n\t\t\t\t...options.stream,\n\t\t\t\tserver,\n\t\t\t\tpath: options.stream.path ?? `${apiPrefix}/stream`,\n\t\t\t})\n\t\t: undefined;\n\toptions.log?.info(\n\t\t`API listening on http://${bind}:${port}${apiPrefix}${\n\t\t\toptions.staticFiles ? \" - web interface is served from disk\" : \"\"\n\t\t}${stream ? \" - live events on /stream\" : \"\"}`,\n\t);\n\n\treturn {\n\t\tport,\n\t\turl: `http://${bind === \"0.0.0.0\" ? \"127.0.0.1\" : bind}:${port}`,\n\t\tstream,\n\t\tclose: async (): Promise<void> => {\n\t\t\tawait stream?.close();\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tserver.close(() => resolve());\n\t\t\t\t// keep-alive connections would delay the shutdown\n\t\t\t\tserver.closeAllConnections();\n\t\t\t});\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b", "_c", "_d", "_e"]
7
+ }