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,151 @@
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 static_exports = {};
30
+ __export(static_exports, {
31
+ createStaticHandler: () => createStaticHandler
32
+ });
33
+ module.exports = __toCommonJS(static_exports);
34
+ var import_node_crypto = require("node:crypto");
35
+ var fs = __toESM(require("node:fs"));
36
+ var path = __toESM(require("node:path"));
37
+ const CONTENT_TYPES = {
38
+ ".html": "text/html; charset=utf-8",
39
+ ".js": "text/javascript; charset=utf-8",
40
+ ".mjs": "text/javascript; charset=utf-8",
41
+ ".css": "text/css; charset=utf-8",
42
+ ".json": "application/json; charset=utf-8",
43
+ ".webmanifest": "application/manifest+json; charset=utf-8",
44
+ ".svg": "image/svg+xml",
45
+ ".png": "image/png",
46
+ ".jpg": "image/jpeg",
47
+ ".jpeg": "image/jpeg",
48
+ ".gif": "image/gif",
49
+ ".ico": "image/x-icon",
50
+ ".woff": "font/woff",
51
+ ".woff2": "font/woff2",
52
+ ".txt": "text/plain; charset=utf-8",
53
+ ".map": "application/json; charset=utf-8"
54
+ };
55
+ function createStaticHandler(options) {
56
+ var _a;
57
+ const root = path.resolve(options.root);
58
+ const indexFile = (_a = options.indexFile) != null ? _a : "index.html";
59
+ return (request) => {
60
+ var _a2, _b, _c;
61
+ const method = ((_a2 = request.method) != null ? _a2 : "GET").toUpperCase();
62
+ if (method !== "GET" && method !== "HEAD") {
63
+ return null;
64
+ }
65
+ let target;
66
+ try {
67
+ target = resolveTarget(root, (_b = request.path) != null ? _b : "/");
68
+ } catch {
69
+ return {
70
+ status: 404,
71
+ headers: { "content-type": "text/plain; charset=utf-8" },
72
+ body: "Not Found"
73
+ };
74
+ }
75
+ let file = target;
76
+ if (!isFile(file)) {
77
+ if (!requestsHtml(request)) {
78
+ return null;
79
+ }
80
+ file = path.join(root, indexFile);
81
+ if (!isFile(file)) {
82
+ return null;
83
+ }
84
+ }
85
+ const stats = fs.statSync(file);
86
+ const etag = `"${(0, import_node_crypto.createHash)("sha1").update(`${stats.size}:${stats.mtimeMs}`).digest("hex")}"`;
87
+ const headers = {
88
+ "content-type": (_c = CONTENT_TYPES[path.extname(file).toLowerCase()]) != null ? _c : "application/octet-stream",
89
+ etag,
90
+ "last-modified": stats.mtime.toUTCString(),
91
+ "cache-control": file.endsWith(indexFile) ? "no-cache" : "public, max-age=3600",
92
+ "x-content-type-options": "nosniff"
93
+ };
94
+ if (isFresh(request, etag, stats.mtime)) {
95
+ return { status: 304, headers, body: "" };
96
+ }
97
+ return {
98
+ status: 200,
99
+ headers: { ...headers, "content-length": String(stats.size) },
100
+ // binary assets must keep their bytes, so the file is read as Buffer
101
+ body: method === "HEAD" ? "" : fs.readFileSync(file)
102
+ };
103
+ };
104
+ }
105
+ function resolveTarget(root, requestPath) {
106
+ const decoded = decodeURIComponent(requestPath.split("?")[0]);
107
+ const relative = decoded.replace(/^\/+/, "");
108
+ const resolved = path.resolve(root, relative);
109
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
110
+ throw new Error("path leaves the root directory");
111
+ }
112
+ return resolved;
113
+ }
114
+ function isFile(file) {
115
+ try {
116
+ return fs.statSync(file).isFile();
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ function requestsHtml(request) {
122
+ var _a;
123
+ const accept = (_a = headerOf(request, "accept")) != null ? _a : "";
124
+ if (!accept) {
125
+ return true;
126
+ }
127
+ return accept.includes("text/html") || accept.includes("*/*");
128
+ }
129
+ function headerOf(request, name) {
130
+ var _a, _b;
131
+ const value = (_a = request.headers) == null ? void 0 : _a[name];
132
+ return Array.isArray(value) ? (_b = value[0]) != null ? _b : null : value != null ? value : null;
133
+ }
134
+ function isFresh(request, etag, modified) {
135
+ const ifNoneMatch = headerOf(request, "if-none-match");
136
+ if (ifNoneMatch) {
137
+ return ifNoneMatch.split(",").some((value) => value.trim() === etag);
138
+ }
139
+ const ifModifiedSince = headerOf(request, "if-modified-since");
140
+ if (ifModifiedSince) {
141
+ const since = Date.parse(ifModifiedSince);
142
+ const modifiedSeconds = Math.floor(modified.getTime() / 1e3) * 1e3;
143
+ return Number.isFinite(since) && modifiedSeconds <= since;
144
+ }
145
+ return false;
146
+ }
147
+ // Annotate the CommonJS export names for ESM import in node:
148
+ 0 && (module.exports = {
149
+ createStaticHandler
150
+ });
151
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/web/static.ts"],
4
+ "sourcesContent": ["/**\n * Static file delivery for the PWA (`www/`).\n *\n * The handler answers `GET`/`HEAD` for files below a root directory. It never leaves that directory: the path\n * is resolved and checked against the root, so `../` or encoded traversal cannot read other files. Single page\n * applications get an `index.html` fallback for unknown paths, but only for HTML requests \u2014 an API call must\n * never receive the HTML page.\n *\n * Caching uses `ETag` and `Last-Modified`, so a reload only transfers changed files; `If-None-Match` and\n * `If-Modified-Since` answer with `304`.\n */\n\nimport { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { HttpRequest, HttpResponse } from \"./router\";\n\n/** Options of the static handler. */\nexport interface StaticFilesOptions {\n\t/** Directory that is served (absolute path) */\n\troot: string;\n\t/** File served for unknown paths of a single page application (default `index.html`) */\n\tindexFile?: string;\n}\n\n/** A static handler: returns a response or `null` when it does not handle the request. */\nexport type StaticHandler = (request: HttpRequest) => HttpResponse | null;\n\nconst CONTENT_TYPES: Record<string, string> = {\n\t\".html\": \"text/html; charset=utf-8\",\n\t\".js\": \"text/javascript; charset=utf-8\",\n\t\".mjs\": \"text/javascript; charset=utf-8\",\n\t\".css\": \"text/css; charset=utf-8\",\n\t\".json\": \"application/json; charset=utf-8\",\n\t\".webmanifest\": \"application/manifest+json; charset=utf-8\",\n\t\".svg\": \"image/svg+xml\",\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".gif\": \"image/gif\",\n\t\".ico\": \"image/x-icon\",\n\t\".woff\": \"font/woff\",\n\t\".woff2\": \"font/woff2\",\n\t\".txt\": \"text/plain; charset=utf-8\",\n\t\".map\": \"application/json; charset=utf-8\",\n};\n\n/**\n * Creates a static file handler.\n *\n * @param options - root directory and index file\n * @returns the handler\n */\nexport function createStaticHandler(options: StaticFilesOptions): StaticHandler {\n\tconst root = path.resolve(options.root);\n\tconst indexFile = options.indexFile ?? \"index.html\";\n\n\treturn (request: HttpRequest): HttpResponse | null => {\n\t\tconst method = (request.method ?? \"GET\").toUpperCase();\n\t\tif (method !== \"GET\" && method !== \"HEAD\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet target: string;\n\t\ttry {\n\t\t\ttarget = resolveTarget(root, request.path ?? \"/\");\n\t\t} catch {\n\t\t\t// traversal attempt: the caller must not learn anything about the file system\n\t\t\treturn {\n\t\t\t\tstatus: 404,\n\t\t\t\theaders: { \"content-type\": \"text/plain; charset=utf-8\" },\n\t\t\t\tbody: \"Not Found\",\n\t\t\t};\n\t\t}\n\n\t\tlet file = target;\n\t\tif (!isFile(file)) {\n\t\t\t// single page application: unknown paths are routed on the client\n\t\t\tif (!requestsHtml(request)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfile = path.join(root, indexFile);\n\t\t\tif (!isFile(file)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst stats = fs.statSync(file);\n\t\tconst etag = `\"${createHash(\"sha1\").update(`${stats.size}:${stats.mtimeMs}`).digest(\"hex\")}\"`;\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"content-type\": CONTENT_TYPES[path.extname(file).toLowerCase()] ?? \"application/octet-stream\",\n\t\t\tetag,\n\t\t\t\"last-modified\": stats.mtime.toUTCString(),\n\t\t\t\"cache-control\": file.endsWith(indexFile) ? \"no-cache\" : \"public, max-age=3600\",\n\t\t\t\"x-content-type-options\": \"nosniff\",\n\t\t};\n\n\t\tif (isFresh(request, etag, stats.mtime)) {\n\t\t\treturn { status: 304, headers, body: \"\" };\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\theaders: { ...headers, \"content-length\": String(stats.size) },\n\t\t\t// binary assets must keep their bytes, so the file is read as Buffer\n\t\t\tbody: method === \"HEAD\" ? \"\" : fs.readFileSync(file),\n\t\t};\n\t};\n}\n\n/**\n * Resolves a request path to a file inside the root directory.\n *\n * @param root - root directory (absolute)\n * @param requestPath - path of the request\n * @returns absolute file path\n */\nfunction resolveTarget(root: string, requestPath: string): string {\n\tconst decoded = decodeURIComponent(requestPath.split(\"?\")[0]);\n\tconst relative = decoded.replace(/^\\/+/, \"\");\n\tconst resolved = path.resolve(root, relative);\n\n\tif (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {\n\t\tthrow new Error(\"path leaves the root directory\");\n\t}\n\t// a directory is mapped to its index file\n\treturn resolved;\n}\n\n/**\n * Checks whether a path is an existing file.\n *\n * @param file - absolute path\n * @returns true when it is a file\n */\nfunction isFile(file: string): boolean {\n\ttry {\n\t\treturn fs.statSync(file).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Checks whether the client expects HTML (a browser navigating, not an API client).\n *\n * @param request - incoming request\n * @returns true when HTML may be delivered\n */\nfunction requestsHtml(request: HttpRequest): boolean {\n\tconst accept = headerOf(request, \"accept\") ?? \"\";\n\tif (!accept) {\n\t\t// no `Accept` header: behave like a browser and deliver the page\n\t\treturn true;\n\t}\n\treturn accept.includes(\"text/html\") || accept.includes(\"*/*\");\n}\n\n/**\n * Reads a header value as string.\n *\n * @param request - incoming request\n * @param name - header name (lower case)\n * @returns the value or `null`\n */\nfunction headerOf(request: HttpRequest, name: string): string | null {\n\tconst value = request.headers?.[name];\n\treturn Array.isArray(value) ? (value[0] ?? null) : (value ?? null);\n}\n\n/**\n * Checks whether the client already has the current version.\n *\n * @param request - incoming request\n * @param etag - current entity tag\n * @param modified - last modification time\n * @returns true when a `304` may be answered\n */\nfunction isFresh(request: HttpRequest, etag: string, modified: Date): boolean {\n\tconst ifNoneMatch = headerOf(request, \"if-none-match\");\n\tif (ifNoneMatch) {\n\t\treturn ifNoneMatch.split(\",\").some(value => value.trim() === etag);\n\t}\n\n\tconst ifModifiedSince = headerOf(request, \"if-modified-since\");\n\tif (ifModifiedSince) {\n\t\tconst since = Date.parse(ifModifiedSince);\n\t\t// `Last-Modified` only carries seconds, so the comparison has to as well\n\t\tconst modifiedSeconds = Math.floor(modified.getTime() / 1000) * 1000;\n\t\treturn Number.isFinite(since) && modifiedSeconds <= since;\n\t}\n\treturn false;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a"]
7
+ }
@@ -0,0 +1,135 @@
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 stream_exports = {};
20
+ __export(stream_exports, {
21
+ attachEventStream: () => attachEventStream
22
+ });
23
+ module.exports = __toCommonJS(stream_exports);
24
+ var import_ws = require("ws");
25
+ var import_cookies = require("./cookies");
26
+ function attachEventStream(options) {
27
+ var _a, _b, _c;
28
+ const path = (_a = options.path) != null ? _a : "/api/stream";
29
+ const now = (_b = options.now) != null ? _b : (() => Math.floor(Date.now() / 1e3));
30
+ const pingIntervalMs = (_c = options.pingIntervalMs) != null ? _c : 3e4;
31
+ const wss = new import_ws.WebSocketServer({ noServer: true });
32
+ const clients = /* @__PURE__ */ new Map();
33
+ function send(socket, payload) {
34
+ if (socket.readyState === socket.OPEN) {
35
+ socket.send(JSON.stringify(payload));
36
+ }
37
+ }
38
+ function reject(socket, status, code) {
39
+ const reason = status === 401 ? "Unauthorized" : "Forbidden";
40
+ const body = JSON.stringify({
41
+ type: "about:blank",
42
+ title: reason,
43
+ status,
44
+ code
45
+ });
46
+ socket.write(
47
+ `HTTP/1.1 ${status} ${reason}\r
48
+ content-type: application/problem+json; charset=utf-8\r
49
+ content-length: ${Buffer.byteLength(body)}\r
50
+ connection: close\r
51
+ \r
52
+ ${body}`
53
+ );
54
+ socket.destroy();
55
+ }
56
+ function textOf(raw) {
57
+ const buffer = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
58
+ return buffer.toString("utf8");
59
+ }
60
+ function onUpgrade(request, socket, head) {
61
+ var _a2, _b2;
62
+ const url = new URL((_a2 = request.url) != null ? _a2 : "/", "http://localhost");
63
+ if (url.pathname !== path) {
64
+ reject(socket, 404, "not_found");
65
+ return;
66
+ }
67
+ const cookieToken = (_b2 = (0, import_cookies.parseCookies)(request.headers.cookie)[import_cookies.SESSION_COOKIE]) != null ? _b2 : "";
68
+ const token = url.searchParams.get("token") || cookieToken;
69
+ const result = options.auth.authenticate({ token, now: now() });
70
+ if (!result.ok) {
71
+ reject(socket, result.error === "permission_denied" ? 403 : 401, result.error);
72
+ return;
73
+ }
74
+ const context = result.context;
75
+ wss.handleUpgrade(request, socket, head, (websocket) => {
76
+ var _a3;
77
+ clients.set(websocket, {
78
+ userId: context.user.id,
79
+ // a manager or administrator sees the whole house, everybody else only themselves
80
+ seesAll: context.permissions.includes("report.view_other")
81
+ });
82
+ send(websocket, {
83
+ type: "hello",
84
+ atUtc: now(),
85
+ userId: context.user.id,
86
+ version: (_a3 = options.version) != null ? _a3 : "0.0.0",
87
+ subscribe: context.permissions.includes("report.view_other") ? "all" : "own"
88
+ });
89
+ websocket.on("close", () => clients.delete(websocket));
90
+ websocket.on("error", () => clients.delete(websocket));
91
+ websocket.on("message", (raw) => {
92
+ if (textOf(raw) === "ping") {
93
+ send(websocket, { type: "pong", atUtc: now() });
94
+ }
95
+ });
96
+ });
97
+ }
98
+ options.server.on("upgrade", onUpgrade);
99
+ const unsubscribe = options.events.subscribe((event) => {
100
+ for (const [socket, client] of clients) {
101
+ if (client.seesAll || event.userId === null || event.userId === client.userId) {
102
+ send(socket, event);
103
+ }
104
+ }
105
+ });
106
+ const pingTimer = options.timers === void 0 ? null : options.timers.setInterval(() => {
107
+ for (const socket of clients.keys()) {
108
+ if (socket.readyState === socket.OPEN) {
109
+ socket.ping();
110
+ }
111
+ }
112
+ }, pingIntervalMs);
113
+ return {
114
+ async close() {
115
+ if (pingTimer !== null && options.timers !== void 0) {
116
+ options.timers.clearInterval(pingTimer);
117
+ }
118
+ unsubscribe();
119
+ options.server.off("upgrade", onUpgrade);
120
+ for (const socket of [...clients.keys()]) {
121
+ socket.close(1001, "server shutting down");
122
+ }
123
+ clients.clear();
124
+ await new Promise((resolve) => wss.close(() => resolve()));
125
+ },
126
+ clientCount() {
127
+ return clients.size;
128
+ }
129
+ };
130
+ }
131
+ // Annotate the CommonJS export names for ESM import in node:
132
+ 0 && (module.exports = {
133
+ attachEventStream
134
+ });
135
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/web/stream.ts"],
4
+ "sourcesContent": ["/**\n * WebSocket endpoint `/api/stream`.\n *\n * Clients connect with their session token as a query parameter (a browser cannot set headers on a WebSocket),\n * the token is checked with the same `AuthService` as the REST API. Each client only receives the events it is\n * allowed to see: its own ones, plus everything when it holds `report.view_other` (a manager or administrator).\n *\n * The connection is kept alive with protocol pings; a client that does not answer them is dropped. There is no\n * history: a client that reconnects reloads the data it needs over REST.\n */\n\nimport type { IncomingMessage, Server } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { WebSocketServer, type RawData, type WebSocket } from \"ws\";\nimport type { AuthService } from \"../services/auth\";\nimport { parseCookies, SESSION_COOKIE } from \"./cookies\";\nimport type { ApiEvent, EventBus } from \"./events\";\n\n/** Options of the stream endpoint. */\nexport interface EventStreamOptions {\n\t/** HTTP server the upgrade requests arrive on */\n\tserver: Server;\n\t/** Path of the endpoint (default `/api/stream`) */\n\tpath?: string;\n\t/** Authentication service used to check the token */\n\tauth: AuthService;\n\t/** Event bus that is forwarded */\n\tevents: EventBus;\n\t/** Reported version in the greeting frame */\n\tversion?: string;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n\t/** Interval of the protocol pings in milliseconds (default 30 s) */\n\tpingIntervalMs?: number;\n\t/**\n\t * Timer functions of the caller. The adapter passes its own (`this.setInterval`/`this.clearInterval`), so\n\t * the ping timer belongs to the adapter and cannot outlive it; without them no ping is sent (tests do not\n\t * need one).\n\t */\n\ttimers?: {\n\t\tsetInterval: (handler: () => void, milliseconds: number) => TimerHandle;\n\t\tclearInterval: (handle: TimerHandle) => void;\n\t};\n}\n\n/**\n * Handle of a repeating timer: the adapter marks its own handles with a brand type, Node returns `Timeout`\n * objects. `null`/`undefined` are allowed because the adapter's own type declaration permits them.\n */\ntype TimerHandle = number | null | undefined | ReturnType<typeof setInterval>;\n\n/** The running stream endpoint. */\nexport interface EventStream {\n\t/** Closes every connection and stops the endpoint */\n\tclose(): Promise<void>;\n\t/** Number of connected clients */\n\tclientCount(): number;\n}\n\n/**\n * Attaches the event stream to an HTTP server.\n *\n * @param options - server, path, authentication and bus\n * @returns the running endpoint\n */\nexport function attachEventStream(options: EventStreamOptions): EventStream {\n\tconst path = options.path ?? \"/api/stream\";\n\tconst now = options.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst pingIntervalMs = options.pingIntervalMs ?? 30_000;\n\tconst wss = new WebSocketServer({ noServer: true });\n\t/** Connected clients with the user they belong to. */\n\tconst clients = new Map<WebSocket, { userId: number; seesAll: boolean }>();\n\n\t/**\n\t * Sends a frame to one client.\n\t *\n\t * @param socket - connection\n\t * @param payload - object to serialise\n\t */\n\tfunction send(socket: WebSocket, payload: unknown): void {\n\t\tif (socket.readyState === socket.OPEN) {\n\t\t\tsocket.send(JSON.stringify(payload));\n\t\t}\n\t}\n\n\t/**\n\t * Rejects an upgrade with a problem document.\n\t *\n\t * @param socket - raw socket of the request\n\t * @param status - HTTP status code\n\t * @param code - stable error code\n\t */\n\tfunction reject(socket: Duplex, status: number, code: string): void {\n\t\tconst reason = status === 401 ? \"Unauthorized\" : \"Forbidden\";\n\t\tconst body = JSON.stringify({\n\t\t\ttype: \"about:blank\",\n\t\t\ttitle: reason,\n\t\t\tstatus,\n\t\t\tcode,\n\t\t});\n\t\tsocket.write(\n\t\t\t`HTTP/1.1 ${status} ${reason}\\r\\n` +\n\t\t\t\t`content-type: application/problem+json; charset=utf-8\\r\\n` +\n\t\t\t\t`content-length: ${Buffer.byteLength(body)}\\r\\n` +\n\t\t\t\t`connection: close\\r\\n\\r\\n${body}`,\n\t\t);\n\t\tsocket.destroy();\n\t}\n\n\t/**\n\t * Reads the text of a received frame.\n\t *\n\t * The library hands the frame over as a `Buffer`, an `ArrayBuffer` or a list of buffers; only the text\n\t * matters here, and a client only ever sends its own ping.\n\t *\n\t * @param raw - received frame\n\t * @returns the frame as UTF-8 text\n\t */\n\tfunction textOf(raw: RawData): string {\n\t\tconst buffer = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n\t\treturn buffer.toString(\"utf8\");\n\t}\n\n\t/**\n\t * Handles an upgrade request of the HTTP server.\n\t *\n\t * @param request - upgrade request\n\t * @param socket - raw socket\n\t * @param head - already read bytes\n\t */\n\tfunction onUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void {\n\t\tconst url = new URL(request.url ?? \"/\", \"http://localhost\");\n\t\tif (url.pathname !== path) {\n\t\t\t// the API owns this path; anything else is not a websocket\n\t\t\treject(socket, 404, \"not_found\");\n\t\t\treturn;\n\t\t}\n\n\t\t// A browser cannot set headers on a handshake, but it does send cookies \u2014 so the session cookie counts as\n\t\t// well and the live stream works without a token in the URL (specification 4.10). The query parameter stays\n\t\t// for terminals and integration clients.\n\t\tconst cookieToken = parseCookies(request.headers.cookie)[SESSION_COOKIE] ?? \"\";\n\t\tconst token = url.searchParams.get(\"token\") || cookieToken;\n\t\tconst result = options.auth.authenticate({ token, now: now() });\n\t\tif (!result.ok) {\n\t\t\treject(socket, result.error === \"permission_denied\" ? 403 : 401, result.error);\n\t\t\treturn;\n\t\t}\n\n\t\tconst context = result.context;\n\t\twss.handleUpgrade(request, socket, head, (websocket: WebSocket) => {\n\t\t\tclients.set(websocket, {\n\t\t\t\tuserId: context.user.id,\n\t\t\t\t// a manager or administrator sees the whole house, everybody else only themselves\n\t\t\t\tseesAll: context.permissions.includes(\"report.view_other\"),\n\t\t\t});\n\n\t\t\tsend(websocket, {\n\t\t\t\ttype: \"hello\",\n\t\t\t\tatUtc: now(),\n\t\t\t\tuserId: context.user.id,\n\t\t\t\tversion: options.version ?? \"0.0.0\",\n\t\t\t\tsubscribe: context.permissions.includes(\"report.view_other\") ? \"all\" : \"own\",\n\t\t\t});\n\n\t\t\twebsocket.on(\"close\", () => clients.delete(websocket));\n\t\t\twebsocket.on(\"error\", () => clients.delete(websocket));\n\t\t\twebsocket.on(\"message\", raw => {\n\t\t\t\t// the only client message that is understood is a ping, everything else is ignored\n\t\t\t\tif (textOf(raw) === \"ping\") {\n\t\t\t\t\tsend(websocket, { type: \"pong\", atUtc: now() });\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\toptions.server.on(\"upgrade\", onUpgrade);\n\n\tconst unsubscribe = options.events.subscribe((event: ApiEvent) => {\n\t\tfor (const [socket, client] of clients) {\n\t\t\tif (client.seesAll || event.userId === null || event.userId === client.userId) {\n\t\t\t\tsend(socket, event);\n\t\t\t}\n\t\t}\n\t});\n\n\t// The adapter hands in its own timers, so the keep-alive ping is cleared with the adapter even when the\n\t// stream is never closed explicitly.\n\tconst pingTimer =\n\t\toptions.timers === undefined\n\t\t\t? null\n\t\t\t: options.timers.setInterval(() => {\n\t\t\t\t\tfor (const socket of clients.keys()) {\n\t\t\t\t\t\tif (socket.readyState === socket.OPEN) {\n\t\t\t\t\t\t\tsocket.ping();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, pingIntervalMs);\n\n\treturn {\n\t\tasync close(): Promise<void> {\n\t\t\tif (pingTimer !== null && options.timers !== undefined) {\n\t\t\t\toptions.timers.clearInterval(pingTimer);\n\t\t\t}\n\t\t\tunsubscribe();\n\t\t\toptions.server.off(\"upgrade\", onUpgrade);\n\t\t\tfor (const socket of [...clients.keys()]) {\n\t\t\t\tsocket.close(1001, \"server shutting down\");\n\t\t\t}\n\t\t\tclients.clear();\n\t\t\tawait new Promise<void>(resolve => wss.close(() => resolve()));\n\t\t},\n\n\t\tclientCount(): number {\n\t\t\treturn clients.size;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b"]
7
+ }