beavuck-time 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,258 +1,264 @@
1
- # ⏲️ Beavuck Time
2
-
3
- [![npm](https://img.shields.io/npm/v/beavuck-time.svg) ![npm](https://img.shields.io/npm/dm/beavuck-time.svg)](https://www.npmjs.com/package/beavuck-time)
4
-
5
- ## 📊 Status
6
-
7
- [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=beavuck-services_time)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
8
-
9
- [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
10
- [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
11
-
12
- [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
13
- [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=bugs)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
14
-
15
- [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
16
- [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
17
- [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
18
-
19
- [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
20
- [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
21
-
22
- [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=coverage)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
23
-
24
- ---
25
-
26
- ## 💡 Why
27
-
28
- You can't count on client devices to all be set up with the correct date and time.
29
-
30
- But keeping track of time is usually busywork, not the business of your core APIs. And you probably don't want to flood
31
- your own APIs, whenever you want to get an accurate timestamp for entities in your apps.
32
-
33
- So you can set up this simple microservice, whose only job should be to answer the question: "what time is it right
34
- now?"
35
-
36
- ---
37
-
38
- ## 🎯 What
39
-
40
- Get the current time in ISO format, in UTC timezone.
41
-
42
- This lightweight service focuses on one job.
43
-
44
- It needs no persistence layer, is capable of handling multiple concurrent requests, and is protected by a simple
45
- CORS config for security and performance reasons.
46
-
47
- Dockerized for easy deployment and scaling.
48
-
49
- ---
50
-
51
- ## 🔍 Where
52
-
53
- The code lives on [GitLab](https://gitlab.com/beavuck-services/time),
54
- the Docker image is hosted on [Docker Hub](https://hub.docker.com/r/beavuck/time),
55
- and it's also stored as a package on [NPM](https://www.npmjs.com/package/beavuck-time)
56
-
57
- ### 🦊 GitLab
58
-
59
- You can find the code on GitLab, where, once you have read the [CONTRIBUTING.md](https://gitlab.com/beavuck-services/time/-/blob/main/CONTRIBUTING.md?ref_type=heads) file, you can also
60
- create issues and merge requests.
61
-
62
- Feel free to fork the repo and make your own changes at will, as per the [UNLICENSE](https://gitlab.com/beavuck-services/time/-/blob/main/UNLICENSE?ref_type=heads).
63
-
64
- ### 🐳 Docker Hub
65
-
66
- Most devs will only use Docker Hub for their purposes with this project, to use it as is as a dependency for their own
67
- projects. On Docker Hub, while you're developing, you should use the `beavuck/time:latest` tag to always get the latest
68
- version.
69
-
70
- When the time comes to go to production, to protect yourself from surprise breaking changes, you should instead point to
71
- specific minor version tags, such as `beavuck/time:3.0` : those will not get breaking changes, but they will get
72
- security upgrades and bug fixes while they're active.
73
-
74
- ### NPM
75
-
76
- You can also use this service directly via npm:
77
-
78
- ```bash
79
- npm install beavuck-time
80
- ```
81
-
82
- #### Programmatically
83
-
84
- If you want to run this service programmatically in your Node.js project:
85
-
86
- ```js
87
- import { startServer } from 'beavuck-time'
88
-
89
- startServer({
90
- hostUrl: 'https://time-api.example.com',
91
- trustedOrigins: 'https://my.app.com, https://my.app.com/*, https://my-other.app.com, https://my-other.app.com/*',
92
- apiPort: 3000
93
- })
94
- ```
95
-
96
- You can rely on environment variables (BEAVUCK_TIME_HOST_URL, etc.) together with (or instead of) passing an options object.
97
- Refer to the Docker Compose example below to see an exhaustive list of available environment variables and what they do.
98
-
99
- #### CLI
100
-
101
- To run this service directly in your command line interface, just run:
102
-
103
- ```bash
104
- npx beavuck-time
105
- ```
106
-
107
- Refer to the Docker Compose example below to see an exhaustive list of available environment variables and what they do.
108
-
109
- ---
110
-
111
- ## ⚙️ Usage
112
-
113
- ### 🪧 Set up (docker-compose example)
114
-
115
- (The section below supposes you're using a Docker image. If using the npm package, you can set those environment variable
116
- on the host directly, instead of doing so in the container like detailed below)
117
-
118
- > **Note:** The Docker image does not include a `.env` file. All configuration must be passed via environment variables at runtime.
119
-
120
- To run the service in a docker-compose environment, add this in your `docker-compose.yml`'s services section:
121
-
122
- ```yaml
123
- time:
124
- image: beavuck/time:latest
125
- ports:
126
- - 'SOME_PORT_NUMBER:3000'
127
- # HOST_PORT:CONTAINER_PORT (Since we are in a container, CONTAINER_PORT corresponds to the API_PORT variable below)
128
- environment:
129
- - BEAVUCK_TIME_HOST_URL: https://time-api.example.com
130
- # BEAVUCK_TIME_HOST_URL: That API's URL. Essential for CORS config.
131
- - BEAVUCK_TIME_TRUSTED_ORIGINS: https://my.app.com,https://my-other.app.com
132
- # BEAVUCK_TIME_TRUSTED_ORIGINS: To allow requests from any origin, include * (not recommended). If empty, will only allow requests from the HOST_URL's origin. Defaults to the HOST_URL's origin
133
- - BEAVUCK_TIME_API_PORT: 3000
134
- # BEAVUCK_TIME_API_PORT: Optional. Internal port when in a container. Defaults to 3000
135
- - BEAVUCK_TIME_RATE_LIMIT: -1
136
- # BEAVUCK_TIME_RATE_LIMIT: Optional. Max allowed number of requests per minute for each IP address. If negative or 0, no limit. Defaults to no limit
137
- - BEAVUCK_TIME_LOG_LEVEL: info
138
- # BEAVUCK_TIME_LOG_LEVEL: Optional. Logging levels include error, warn, info, http, verbose, debug, silly. Defaults to info
139
- - BEAVUCK_TIME_MAX_LOG_FILES: 64
140
- # BEAVUCK_TIME_MAX_LOG_FILES: Optional. Maximum number of logs to keep. This can be a number of files or number of days. If using days, add 'd' as the suffix. Default is 64
141
- - BEAVUCK_TIME_MAX_SIZE_LOG_FILES: 1m
142
- # BEAVUCK_TIME_MAX_SIZE_LOG_FILES: Optional. Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb. If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number. Default is 1m
143
- ```
144
-
145
- Here's the simple docker compose file I used to test this service locally:
146
-
147
- ```yaml
148
- services:
149
- time:
150
- image: beavuck/time:latest
151
- ports:
152
- - '3000:3000'
153
- environment:
154
- BEAVUCK_TIME_HOST_URL: http://127.0.0.1:3000
155
- BEAVUCK_TIME_TRUSTED_ORIGINS: http://127.0.0.1:8000,http://localhost:8000
156
- BEAVUCK_TIME_LOG_LEVEL: debug
157
- ```
158
-
159
- When you're ready, just run your services with:
160
-
161
- ```shell
162
- docker compose up -d
163
- ```
164
-
165
- ### ✨ Using the service
166
-
167
- Now, when you run:
168
-
169
- ```
170
- curl --location 'http://localhost:{{SOME_PORT_NUMBER}}/now' \
171
- --header 'Origin: {{SOME_TRUSTED_ORIGIN}}'
172
- ```
173
-
174
- you should expect an answer such as:
175
-
176
- ```json
177
- {
178
- "now": "2024-06-15T12:35:48.022Z"
179
- }
180
- ```
181
-
182
- ---
183
-
184
- ## 🛡️ CORS
185
-
186
- This service is protected by a CORS policy, which you can configure by setting the `TRUSTED_ORIGINS` environment
187
- variable.
188
-
189
- When you use the API, keep in mind what roles these headers play:
190
-
191
- | `"Origin:"` | `"Referrer:"`* | Result |
192
- | --------------------------------------------------------- | ------------------------------ | ------ |
193
- | Defined and API `BEAVUCK_TIME_TRUSTED_ORIGINS` set to `*` | Whatever | ✅ |
194
- | Trusted | Whatever | ✅ |
195
- | Same as this API's host | Whatever | ✅ |
196
- | Not defined | Same as this API's host | ✅ |
197
- | Defined and not trusted | Whatever | 🛑 |
198
- | Not defined | Not defined | 🛑 |
199
- | Not defined or not trusted | Different from this API's host | 🛑 |
200
-
201
- *AKA `Referer` (sic).
202
-
203
- ---
204
-
205
- ## 📚 Use cases
206
-
207
- ### Batch `POST`s
208
-
209
- Suppose you're creating timestamped entities in your app, and you want to send them in a batch to your API. Your API
210
- knows the current time when it gets the request, but not the creation time of each entity.
211
-
212
- ```mermaid
213
- sequenceDiagram
214
- participant 📱 Client
215
- participant 🖥 Core API
216
- 📱 Client->>🖥 Core API: POST /entities [{...}, {...}, {...}]
217
- ```
218
-
219
- And querying your core API for the current time for each entity is a waste of resources -- that's why you're batching
220
- the operation in the first place.
221
-
222
- ```mermaid
223
- sequenceDiagram
224
- participant 📱 Client
225
- participant 🖥 Core API
226
- 📱 Client->>🖥 Core API: GET /now
227
- 🖥 Core API->>📱 Client: {"now": "2024-06-15T12:35:48.022Z"}
228
- 📱 Client->>🖥 Core API: GET /now
229
- 🖥 Core API->>📱 Client: {"now": "2024-06-15T12:35:49.071Z"}
230
- 📱 Client->>🖥 Core API: GET /now
231
- 🖥 Core API->>📱 Client: {"now": "2024-06-15T12:35:49.243Z"}
232
- 📱 Client->>🖥 Core API: POST /entities [{...}, {...}, {...}]
233
- ```
234
-
235
- So you can use this service to get the current time whenever you need it, and use that as the creation time for each
236
- entity.
237
-
238
- ```mermaid
239
- sequenceDiagram
240
- participant 📱 Client
241
- participant 🕒 Time
242
- participant 🖥 Core API
243
- 📱 Client->>🕒 Time: GET /now
244
- 🕒 Time->>📱 Client: {"now": "2024-06-15T12:35:48.022Z"}
245
- 📱 Client->>🕒 Time: GET /now
246
- 🕒 Time->>📱 Client: {"now": "2024-06-15T12:35:49.071Z"}
247
- 📱 Client->>🕒 Time: GET /now
248
- 🕒 Time->>📱 Client: {"now": "2024-06-15T12:35:49.243Z"}
249
- 📱 Client->>🖥 Core API: POST /entities [{...}, {...}, {...}]
250
- ```
251
-
252
- ---
253
-
254
- ## 📜 License
255
-
256
- Have at it.
257
-
258
- This project uses the Unlicense. See the [UNLICENSE](https://gitlab.com/beavuck-services/time/-/blob/main/UNLICENSE?ref_type=heads) file for details.
1
+ # ⏲️ Beavuck Time
2
+
3
+ [![npm](https://img.shields.io/npm/v/beavuck-time.svg) ![npm](https://img.shields.io/npm/dm/beavuck-time.svg)](https://www.npmjs.com/package/beavuck-time)
4
+
5
+ ## 📊 Status
6
+
7
+ [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=beavuck-services_time)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
8
+
9
+ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
10
+ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
11
+
12
+ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
13
+ [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=bugs)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
14
+
15
+ [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
16
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
17
+ [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
18
+
19
+ [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
20
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
21
+
22
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=beavuck-services_time&metric=coverage)](https://sonarcloud.io/summary/new_code?id=beavuck-services_time)
23
+
24
+ ---
25
+
26
+ ## 💡 Why
27
+
28
+ You can't count on client devices to all be set up with the correct date and time.
29
+
30
+ But keeping track of time is usually busywork, not the business of your core APIs. And you probably don't want to flood
31
+ your own APIs, whenever you want to get an accurate timestamp for entities in your apps.
32
+
33
+ So you can set up this simple microservice, whose only job should be to answer the question: "what time is it right
34
+ now?"
35
+
36
+ ---
37
+
38
+ ## 🎯 What
39
+
40
+ Get the current time in ISO format, in UTC timezone.
41
+
42
+ This lightweight service focuses on one job.
43
+
44
+ It needs no persistence layer, is capable of handling multiple concurrent requests, and is protected by a simple
45
+ CORS config for security and performance reasons.
46
+
47
+ Dockerized for easy deployment and scaling.
48
+
49
+ ---
50
+
51
+ ## 🔍 Where
52
+
53
+ The code lives on [GitLab](https://gitlab.com/beavuck-services/time),
54
+ the Docker image is hosted on [Docker Hub](https://hub.docker.com/r/beavuck/time),
55
+ and it's also stored as a package on [NPM](https://www.npmjs.com/package/beavuck-time)
56
+
57
+ ### 🦊 GitLab
58
+
59
+ You can find the code on GitLab, where, once you have read
60
+ the [CONTRIBUTING.md](https://gitlab.com/beavuck-services/time/-/blob/main/CONTRIBUTING.md?ref_type=heads) file, you can
61
+ also create issues and merge requests.
62
+
63
+ Feel free to fork the repo and make your own changes at will, as per
64
+ the [UNLICENSE](https://gitlab.com/beavuck-services/time/-/blob/main/UNLICENSE?ref_type=heads).
65
+
66
+ ### 🐳 Docker Hub
67
+
68
+ Most devs will only use Docker Hub for their purposes with this project, to use it as is as a dependency for their own
69
+ projects. On Docker Hub, while you're developing, you should use the `beavuck/time:latest` tag to always get the latest
70
+ version.
71
+
72
+ When the time comes to go to production, to protect yourself from surprise breaking changes, you should instead point to
73
+ specific minor version tags, such as `beavuck/time:3.0` : those will not get breaking changes, but they will get
74
+ security upgrades and bug fixes while they're active.
75
+
76
+ ### NPM
77
+
78
+ You can also use this service directly via npm:
79
+
80
+ ```bash
81
+ npm install beavuck-time
82
+ ```
83
+
84
+ #### Programmatically
85
+
86
+ If you want to run this service programmatically in your Node.js project:
87
+
88
+ ```js
89
+ import { startServer } from 'beavuck-time'
90
+
91
+ startServer({
92
+ hostUrl: 'https://time-api.example.com',
93
+ trustedOrigins: 'https://my.app.com, https://my.app.com/*, https://my-other.app.com, https://my-other.app.com/*',
94
+ apiPort: 3000
95
+ })
96
+ ```
97
+
98
+ You can rely on environment variables (BEAVUCK_TIME_HOST_URL, etc.) together with (or instead of) passing an options
99
+ object. Refer to the Docker Compose example below to see an exhaustive list of available environment variables and what
100
+ they do.
101
+
102
+ #### CLI
103
+
104
+ To run this service directly in your command line interface, just run:
105
+
106
+ ```bash
107
+ npx beavuck-time
108
+ ```
109
+
110
+ Refer to the Docker Compose example below to see an exhaustive list of available environment variables and what they do.
111
+
112
+ ---
113
+
114
+ ## ⚙️ Usage
115
+
116
+ ### 🪧 Set up (docker-compose example)
117
+
118
+ (The section below supposes you're using a Docker image. If using the npm package, you can set those environment
119
+ variable
120
+ on the host directly, instead of doing so in the container like detailed below)
121
+
122
+ > **Note:** The Docker image does not include a `.env` file. All configuration must be passed via environment variables
123
+ > at runtime.
124
+
125
+ To run the service in a docker-compose environment, add this in your `docker-compose.yml`'s services section:
126
+
127
+ ```yaml
128
+ time:
129
+ image: beavuck/time:latest
130
+ ports:
131
+ - 'SOME_PORT_NUMBER:3000'
132
+ # HOST_PORT:CONTAINER_PORT (Since we are in a container, CONTAINER_PORT corresponds to the API_PORT variable below)
133
+ environment:
134
+ - BEAVUCK_TIME_HOST_URL: https://time-api.example.com
135
+ # BEAVUCK_TIME_HOST_URL: That API's URL. Essential for CORS config.
136
+ - BEAVUCK_TIME_TRUSTED_ORIGINS: https://my.app.com,https://my-other.app.com
137
+ # BEAVUCK_TIME_TRUSTED_ORIGINS: To allow requests from any origin, include * (not recommended). If empty, will only allow requests from the HOST_URL's origin. Defaults to the HOST_URL's origin
138
+ - BEAVUCK_TIME_API_PORT: 3000
139
+ # BEAVUCK_TIME_API_PORT: Optional. Internal port when in a container. Defaults to 3000
140
+ - BEAVUCK_TIME_RATE_LIMIT: -1
141
+ # BEAVUCK_TIME_RATE_LIMIT: Optional. Max allowed number of requests per minute for each IP address. If negative or 0, no limit. Defaults to no limit
142
+ - BEAVUCK_TIME_LOG_LEVEL: info
143
+ # BEAVUCK_TIME_LOG_LEVEL: Optional. Logging levels include error, warn, info, http, verbose, debug, silly. Defaults to info
144
+ - BEAVUCK_TIME_MAX_LOG_FILES: 64
145
+ # BEAVUCK_TIME_MAX_LOG_FILES: Optional. Maximum number of logs to keep. This can be a number of files or number of days. If using days, add 'd' as the suffix. Default is 64
146
+ - BEAVUCK_TIME_MAX_SIZE_LOG_FILES: 1m
147
+ # BEAVUCK_TIME_MAX_SIZE_LOG_FILES: Optional. Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb. If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number. Default is 1m
148
+ ```
149
+
150
+ Here's the simple docker compose file I used to test this service locally:
151
+
152
+ ```yaml
153
+ services:
154
+ time:
155
+ image: beavuck/time:latest
156
+ ports:
157
+ - '3000:3000'
158
+ environment:
159
+ BEAVUCK_TIME_HOST_URL: http://127.0.0.1:3000
160
+ BEAVUCK_TIME_TRUSTED_ORIGINS: http://127.0.0.1:8000,http://localhost:8000
161
+ BEAVUCK_TIME_LOG_LEVEL: debug
162
+ ```
163
+
164
+ When you're ready, just run your services with:
165
+
166
+ ```shell
167
+ docker compose up -d
168
+ ```
169
+
170
+ ### Using the service
171
+
172
+ Now, when you run:
173
+
174
+ ```
175
+ curl --location 'http://localhost:{{SOME_PORT_NUMBER}}/now' \
176
+ --header 'Origin: {{SOME_TRUSTED_ORIGIN}}'
177
+ ```
178
+
179
+ you should expect an answer such as:
180
+
181
+ ```json
182
+ {
183
+ "now": "2024-06-15T12:35:48.022Z"
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🛡️ CORS
190
+
191
+ This service is protected by a CORS policy, which you can configure by setting the `TRUSTED_ORIGINS` environment
192
+ variable.
193
+
194
+ When you use the API, keep in mind what roles these headers play:
195
+
196
+ | `"Origin:"` | `"Referrer:"`* | Result |
197
+ |-----------------------------------------------------------|--------------------------------|--------|
198
+ | Defined and API `BEAVUCK_TIME_TRUSTED_ORIGINS` set to `*` | Whatever | ✅ |
199
+ | Trusted | Whatever | |
200
+ | Same as this API's host | Whatever | ✅ |
201
+ | Not defined | Same as this API's host | ✅ |
202
+ | Defined and not trusted | Whatever | 🛑 |
203
+ | Not defined | Not defined | 🛑 |
204
+ | Not defined or not trusted | Different from this API's host | 🛑 |
205
+
206
+ *AKA `Referer` (sic).
207
+
208
+ ---
209
+
210
+ ## 📚 Use cases
211
+
212
+ ### Batch `POST`s
213
+
214
+ Suppose you're creating timestamped entities in your app, and you want to send them in a batch to your API. Your API
215
+ knows the current time when it gets the request, but not the creation time of each entity.
216
+
217
+ ```mermaid
218
+ sequenceDiagram
219
+ participant 📱 Client
220
+ participant 🖥 Core API
221
+ 📱 Client ->> 🖥 Core API: POST /entities [{...}, {...}, {...}]
222
+ ```
223
+
224
+ And querying your core API for the current time for each entity is a waste of resources -- that's why you're batching
225
+ the operation in the first place.
226
+
227
+ ```mermaid
228
+ sequenceDiagram
229
+ participant 📱 Client
230
+ participant 🖥 Core API
231
+ 📱 Client ->> 🖥 Core API: GET /now
232
+ 🖥 Core API ->> 📱 Client: {"now": "2024-06-15T12:35:48.022Z"}
233
+ 📱 Client ->> 🖥 Core API: GET /now
234
+ 🖥 Core API ->> 📱 Client: {"now": "2024-06-15T12:35:49.071Z"}
235
+ 📱 Client ->> 🖥 Core API: GET /now
236
+ 🖥 Core API ->> 📱 Client: {"now": "2024-06-15T12:35:49.243Z"}
237
+ 📱 Client ->> 🖥 Core API: POST /entities [{...}, {...}, {...}]
238
+ ```
239
+
240
+ So you can use this service to get the current time whenever you need it, and use that as the creation time for each
241
+ entity.
242
+
243
+ ```mermaid
244
+ sequenceDiagram
245
+ participant 📱 Client
246
+ participant 🕒 Time
247
+ participant 🖥 Core API
248
+ 📱 Client ->> 🕒 Time: GET /now
249
+ 🕒 Time ->> 📱 Client: {"now": "2024-06-15T12:35:48.022Z"}
250
+ 📱 Client ->> 🕒 Time: GET /now
251
+ 🕒 Time ->> 📱 Client: {"now": "2024-06-15T12:35:49.071Z"}
252
+ 📱 Client ->> 🕒 Time: GET /now
253
+ 🕒 Time ->> 📱 Client: {"now": "2024-06-15T12:35:49.243Z"}
254
+ 📱 Client ->> 🖥 Core API: POST /entities [{...}, {...}, {...}]
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 📜 License
260
+
261
+ Have at it.
262
+
263
+ This project uses the Unlicense. See
264
+ the [UNLICENSE](https://gitlab.com/beavuck-services/time/-/blob/main/UNLICENSE?ref_type=heads) file for details.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const supertest_1 = __importDefault(require("supertest"));
7
+ const api_1 = require("../api");
8
+ const http_status_codes_1 = require("http-status-codes");
9
+ const isoTimestamp_1 = require("../types/isoTimestamp");
10
+ function getSomeTrustedOrigin() {
11
+ return process.env.BEAVUCK_TIME_TRUSTED_ORIGINS?.split(',')[0] ?? '';
12
+ }
13
+ /**
14
+ * Increments the callsMadeSoFar counter and returns the response and trusted origin
15
+ */
16
+ async function makeSuccessfulCallToNowEndpoint() {
17
+ const trustedOrigin = getSomeTrustedOrigin();
18
+ const res = await (0, supertest_1.default)(api_1.api).get('/now').set('Origin', trustedOrigin);
19
+ return { res, trustedOrigin };
20
+ }
21
+ describe('App Initialization', () => {
22
+ it('should disable x-powered-by header', async () => {
23
+ const res = await (0, supertest_1.default)(api_1.api).get('/now');
24
+ expect(res.header['x-powered-by']).toBeUndefined();
25
+ });
26
+ it('should allow requests from trusted origins', async () => {
27
+ const { res, trustedOrigin } = await makeSuccessfulCallToNowEndpoint();
28
+ expect(res.header['access-control-allow-origin']).toBe(trustedOrigin);
29
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.OK);
30
+ });
31
+ it('should block requests from non-trusted origins', async () => {
32
+ const res = await (0, supertest_1.default)(api_1.api).get('/now').set('Origin', 'https://non-trusted.com');
33
+ expect(res.header['access-control-allow-origin']).toBeUndefined();
34
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.FORBIDDEN);
35
+ });
36
+ it('should block requests with no origin header', async () => {
37
+ const res = await (0, supertest_1.default)(api_1.api).get('/now');
38
+ expect(res.header['access-control-allow-origin']).toBeUndefined();
39
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.FORBIDDEN);
40
+ });
41
+ it('should allow requests with no origin header but a referrer header identical to server url', async () => {
42
+ const REFERRER = process.env.BEAVUCK_TIME_HOST_URL;
43
+ const res = await (0, supertest_1.default)(api_1.api).get('/now').set('Referrer', REFERRER);
44
+ expect(res.header['access-control-allow-origin']).toBeUndefined();
45
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.OK);
46
+ });
47
+ it('should allow requests with no origin header but a referer (sic) header identical to server url', async () => {
48
+ const REFERER = process.env.BEAVUCK_TIME_HOST_URL;
49
+ const res = await (0, supertest_1.default)(api_1.api).get('/now').set('Referer', REFERER);
50
+ expect(res.header['access-control-allow-origin']).toBeUndefined();
51
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.OK);
52
+ });
53
+ it('should handle errors using errorHandler middleware', async () => {
54
+ const res = await (0, supertest_1.default)(api_1.api).get('/nope').set('Origin', getSomeTrustedOrigin());
55
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.NOT_FOUND);
56
+ });
57
+ it('should return the current time in ISO format', async () => {
58
+ const { res } = await makeSuccessfulCallToNowEndpoint();
59
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.OK);
60
+ expect(res.body).toHaveProperty('now');
61
+ expect(res.body.now).toMatch(isoTimestamp_1.RFC_3339_FORMAT);
62
+ });
63
+ it('should return 500 if HOST_URL is not set', async () => {
64
+ const originalHostUrl = process.env.BEAVUCK_TIME_HOST_URL;
65
+ delete process.env.BEAVUCK_TIME_HOST_URL;
66
+ const res = await (0, supertest_1.default)(api_1.api).get('/now');
67
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR);
68
+ process.env.BEAVUCK_TIME_HOST_URL = originalHostUrl;
69
+ });
70
+ it('should block requests with non-trusted referrer header if origin is not OK', async () => {
71
+ const res = await (0, supertest_1.default)(api_1.api).get('/now').set('Referrer', 'https://non-trusted.com');
72
+ expect(res.status).toBe(http_status_codes_1.StatusCodes.FORBIDDEN);
73
+ });
74
+ });
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ // src/__tests__/config/corsOptions.test.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const corsOptions_1 = require("../../config/corsOptions");
5
+ function setTrustedOrigins(...origins) {
6
+ process.env.BEAVUCK_TIME_TRUSTED_ORIGINS = origins.join(',');
7
+ (0, corsOptions_1.initTrustedOrigins)();
8
+ }
9
+ describe('isOriginTrusted', () => {
10
+ it('returns true for an exact match', () => {
11
+ setTrustedOrigins('https://app.example.com');
12
+ expect((0, corsOptions_1.isOriginTrusted)('https://app.example.com')).toBe(true);
13
+ });
14
+ it('returns true for a wildcard pattern matching the origin host', () => {
15
+ setTrustedOrigins('https://app.example.com/*');
16
+ expect((0, corsOptions_1.isOriginTrusted)('https://app.example.com')).toBe(true);
17
+ });
18
+ it('does not allow a domain that merely shares a prefix with a trusted origin', () => {
19
+ setTrustedOrigins('https://app.example.com');
20
+ expect((0, corsOptions_1.isOriginTrusted)('https://app.example.com.evil.com')).toBe(false);
21
+ });
22
+ it('does not allow a domain that shares a prefix with a wildcard trusted origin', () => {
23
+ setTrustedOrigins('https://app.example.com/*');
24
+ expect((0, corsOptions_1.isOriginTrusted)('https://app.example.com.evil.com')).toBe(false);
25
+ });
26
+ it('returns false for an untrusted origin', () => {
27
+ setTrustedOrigins('https://app.example.com');
28
+ expect((0, corsOptions_1.isOriginTrusted)('https://other.com')).toBe(false);
29
+ });
30
+ it('returns false when the trusted origin pattern is malformed', () => {
31
+ setTrustedOrigins('not-a-url/*');
32
+ expect((0, corsOptions_1.isOriginTrusted)('not-a-url')).toBe(false);
33
+ });
34
+ });
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ // src/__tests__/middlewares/corsMiddleware.test.ts
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const cors_1 = __importDefault(require("cors"));
8
+ const corsMiddleware_1 = require("../../middlewares/corsMiddleware");
9
+ const corsError_1 = require("../../errors/corsError");
10
+ const beavuckTimeServerError_1 = require("../../errors/beavuckTimeServerError");
11
+ const logger_1 = require("../../config/logger");
12
+ jest.mock('cors');
13
+ jest.mock('../../config/logger');
14
+ const mockCors = cors_1.default;
15
+ describe('corsMiddleware', () => {
16
+ let req;
17
+ let res;
18
+ let next;
19
+ beforeEach(() => {
20
+ req = { headers: {} };
21
+ res = {};
22
+ next = jest.fn();
23
+ mockCors.mockReturnValue((_req, _res, n) => n());
24
+ process.env.BEAVUCK_TIME_HOST_URL = 'http://localhost:3000';
25
+ });
26
+ afterEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+ it('delegates to cors() when origin header is present', () => {
30
+ req.headers.origin = 'http://trusted.com';
31
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
32
+ expect(mockCors).toHaveBeenCalled();
33
+ expect(next).toHaveBeenCalledWith();
34
+ });
35
+ it('logs the sanitized origin on CORS requests', () => {
36
+ req.headers.origin = 'http://evil.com\x01injected';
37
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
38
+ expect(logger_1.logger.debug).toHaveBeenCalledWith('CORS request from http://evil.cominjected');
39
+ });
40
+ it('calls next with BeavuckTimeServerError when HOST_URL is not set', () => {
41
+ delete process.env.BEAVUCK_TIME_HOST_URL;
42
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
43
+ expect(next).toHaveBeenCalledWith(expect.any(beavuckTimeServerError_1.BeavuckTimeServerError));
44
+ });
45
+ it('calls next without error when referrer matches the host', () => {
46
+ req.headers.referer = 'http://localhost:3000/page';
47
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
48
+ expect(next).toHaveBeenCalledWith();
49
+ });
50
+ it('calls next with CorsError when neither origin nor referrer is trusted', () => {
51
+ req.headers.referer = 'http://untrusted.com';
52
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
53
+ expect(next).toHaveBeenCalledWith(expect.any(corsError_1.CorsError));
54
+ });
55
+ it('sanitizes control characters from the CorsError message', () => {
56
+ req.headers.referer = 'http://evil.com\x0ainjected';
57
+ (0, corsMiddleware_1.corsMiddleware)(req, res, next);
58
+ const error = next.mock.calls[0][0];
59
+ // eslint-disable-next-line no-control-regex
60
+ expect(error.message).not.toMatch(/\x0a/);
61
+ expect(error.message).toContain('http://evil.cominjected');
62
+ });
63
+ });
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // src/__tests__/middlewares/errorHandler.test.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const errorHandler_1 = require("../../middlewares/errorHandler");
5
+ const beavuckTimeClientError_1 = require("../../errors/beavuckTimeClientError");
6
+ const beavuckTimeServerError_1 = require("../../errors/beavuckTimeServerError");
7
+ const corsError_1 = require("../../errors/corsError");
8
+ const http_status_codes_1 = require("http-status-codes");
9
+ const logger_1 = require("../../config/logger");
10
+ const tsoa_1 = require("tsoa");
11
+ jest.mock('../../config/logger');
12
+ describe('errorHandler', () => {
13
+ let req;
14
+ let res;
15
+ let next;
16
+ beforeEach(() => {
17
+ req = {};
18
+ res = {
19
+ status: jest.fn().mockReturnThis(),
20
+ json: jest.fn().mockReturnThis(),
21
+ };
22
+ next = jest.fn();
23
+ });
24
+ it('should handle BeavuckTimeClientError', () => {
25
+ const err = new beavuckTimeClientError_1.BeavuckTimeClientError('Some message');
26
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
27
+ expect(logger_1.logger.warn).toHaveBeenCalledWith(err);
28
+ expect(res.status).toHaveBeenCalledWith(err.code);
29
+ expect(res.json).toHaveBeenCalledWith({ message: `${err.message}` });
30
+ });
31
+ it('should handle CorsError', () => {
32
+ const err = new corsError_1.CorsError('Some message');
33
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
34
+ expect(logger_1.logger.warn).toHaveBeenCalledWith(err);
35
+ expect(res.status).toHaveBeenCalledWith(err.code);
36
+ expect(res.json).toHaveBeenCalledWith({ message: `${err.message}` });
37
+ });
38
+ it('should handle BeavuckTimeServerError', () => {
39
+ const err = new beavuckTimeServerError_1.BeavuckTimeServerError('Some message');
40
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
41
+ expect(logger_1.logger.error).toHaveBeenCalledWith(err);
42
+ expect(res.status).toHaveBeenCalledWith(err.code);
43
+ expect(res.json).toHaveBeenCalledWith({ message: beavuckTimeServerError_1.BeavuckTimeServerError.baseMessage });
44
+ });
45
+ it('should handle non-specific error', () => {
46
+ const err = new Error('Some message');
47
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
48
+ const beavuckError = beavuckTimeServerError_1.BeavuckTimeServerError.fromError(err);
49
+ expect(logger_1.logger.error).toHaveBeenCalledWith(beavuckError);
50
+ expect(res.status).toHaveBeenCalledWith(http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR);
51
+ expect(res.json).toHaveBeenCalledWith({ message: beavuckTimeServerError_1.BeavuckTimeServerError.baseMessage });
52
+ });
53
+ it('should call next if no error', () => {
54
+ (0, errorHandler_1.errorHandler)(undefined, req, res, next);
55
+ expect(next).toHaveBeenCalled();
56
+ });
57
+ it('should handle ValidateError', () => {
58
+ const err = new tsoa_1.ValidateError({}, 'Some message');
59
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
60
+ expect(logger_1.logger.error).toHaveBeenCalled();
61
+ expect(res.status).toHaveBeenCalledWith(http_status_codes_1.StatusCodes.UNPROCESSABLE_ENTITY);
62
+ expect(res.json).toHaveBeenCalledWith({
63
+ message: 'Validation Error',
64
+ details: err.fields,
65
+ });
66
+ });
67
+ it('should handle unknown error type', () => {
68
+ const err = { some: 'unknown error' };
69
+ (0, errorHandler_1.errorHandler)(err, req, res, next);
70
+ expect(logger_1.logger.error).toHaveBeenCalled();
71
+ expect(res.status).toHaveBeenCalledWith(http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR);
72
+ expect(res.json).toHaveBeenCalledWith({
73
+ message: 'Internal Server Error',
74
+ });
75
+ });
76
+ it('should handle null error', () => {
77
+ (0, errorHandler_1.errorHandler)(undefined, req, res, next);
78
+ expect(next).toHaveBeenCalled();
79
+ });
80
+ });
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ // src/__tests__/utils/stringUtil.test.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const stringUtil_1 = require("../../utils/stringUtil");
5
+ describe('sanitizeForLog', () => {
6
+ it('strips C0 control characters', () => {
7
+ expect((0, stringUtil_1.sanitizeForLog)('foo\x00\x01\x1fbar')).toBe('foobar');
8
+ });
9
+ it('strips DEL and C1 control characters', () => {
10
+ expect((0, stringUtil_1.sanitizeForLog)('foo\x7f\x80\x9fbar')).toBe('foobar');
11
+ });
12
+ it('leaves printable characters untouched', () => {
13
+ expect((0, stringUtil_1.sanitizeForLog)('https://evil.com')).toBe('https://evil.com');
14
+ });
15
+ });
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ // src/__tests__/utils/urlUtil.test.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const urlUtil_1 = require("../../utils/urlUtil");
5
+ const node_url_1 = require("node:url");
6
+ const A_VALID_URL_STRING = 'https://example.com:1234';
7
+ const SAME_VALID_URL_DIFFERENT_PORT = 'https://example.com:81';
8
+ const SOME_OTHER_VALID_URL_STRING = 'https://different.com:1234';
9
+ const AN_INVALID_URL_STRING = 'invalid';
10
+ describe('urlUtil', () => {
11
+ describe('isSameOrigin', () => {
12
+ it('returns false when either URL is undefined', () => {
13
+ const url = new node_url_1.URL(A_VALID_URL_STRING);
14
+ expect((0, urlUtil_1.isSameOrigin)(url)).toBe(false);
15
+ expect((0, urlUtil_1.isSameOrigin)(undefined, url)).toBe(false);
16
+ });
17
+ it('returns true when URLs have the same origin', () => {
18
+ const url1 = new node_url_1.URL(A_VALID_URL_STRING);
19
+ const url2 = new node_url_1.URL(A_VALID_URL_STRING);
20
+ expect((0, urlUtil_1.isSameOrigin)(url1, url2)).toBe(true);
21
+ });
22
+ it('returns false when URLs have the same origin but different ports', () => {
23
+ const url1 = new node_url_1.URL(A_VALID_URL_STRING);
24
+ const url2 = new node_url_1.URL(SAME_VALID_URL_DIFFERENT_PORT);
25
+ expect((0, urlUtil_1.isSameOrigin)(url1, url2)).toBe(false);
26
+ });
27
+ it('returns false when URLs have different origins', () => {
28
+ const url1 = new node_url_1.URL(A_VALID_URL_STRING);
29
+ const url2 = new node_url_1.URL(SOME_OTHER_VALID_URL_STRING);
30
+ expect((0, urlUtil_1.isSameOrigin)(url1, url2)).toBe(false);
31
+ });
32
+ });
33
+ describe('tryParseUrl', () => {
34
+ it('returns undefined when URL string is undefined', () => {
35
+ expect((0, urlUtil_1.tryParseUrl)()).toBeUndefined();
36
+ });
37
+ it('returns a URL object when URL string is valid', () => {
38
+ const url = (0, urlUtil_1.tryParseUrl)(A_VALID_URL_STRING);
39
+ expect(url).toBeInstanceOf(node_url_1.URL);
40
+ expect(url?.origin).toBe(A_VALID_URL_STRING);
41
+ });
42
+ it('returns undefined when URL string is invalid', () => {
43
+ expect((0, urlUtil_1.tryParseUrl)(AN_INVALID_URL_STRING)).toBeUndefined();
44
+ });
45
+ });
46
+ });
@@ -7,7 +7,7 @@ exports.initHostUrl = initHostUrl;
7
7
  exports.getTrustedOrigins = getTrustedOrigins;
8
8
  exports.initTrustedOrigins = initTrustedOrigins;
9
9
  exports.isAllTrusted = isAllTrusted;
10
- exports.isOriginAbsentOrTrusted = isOriginAbsentOrTrusted;
10
+ exports.isOriginTrusted = isOriginTrusted;
11
11
  const corsError_1 = require("../errors/corsError");
12
12
  const urlUtil_1 = require("../utils/urlUtil");
13
13
  const http_status_codes_1 = require("http-status-codes");
@@ -15,7 +15,7 @@ exports.corsOptions = {
15
15
  methods: ['GET', 'OPTIONS'],
16
16
  optionsSuccessStatus: http_status_codes_1.StatusCodes.OK,
17
17
  origin: (origin, callback) => {
18
- if (isAllTrusted() || (0, urlUtil_1.isSameOrigin)(getHostUrl(), (0, urlUtil_1.tryParseUrl)(origin)) || isOriginAbsentOrTrusted(origin)) {
18
+ if (!origin || isAllTrusted() || (0, urlUtil_1.isSameOrigin)(getHostUrl(), (0, urlUtil_1.tryParseUrl)(origin)) || isOriginTrusted(origin)) {
19
19
  // eslint-disable-next-line no-restricted-syntax -- the http external library expects null
20
20
  callback(null, true);
21
21
  }
@@ -51,8 +51,21 @@ function initTrustedOrigins() {
51
51
  function isAllTrusted() {
52
52
  return getTrustedOrigins().includes('*');
53
53
  }
54
- function isOriginAbsentOrTrusted(origin) {
55
- return (!origin ||
56
- getTrustedOrigins().includes(origin) ||
57
- getTrustedOrigins().some(to => origin.startsWith(to.replace('/*', ''))));
54
+ function isOriginTrusted(origin) {
55
+ if (getTrustedOrigins().includes(origin)) {
56
+ return true;
57
+ }
58
+ return getTrustedOrigins().some(trusted => {
59
+ const trustedPattern = trusted.replace(/\/\*$/, '');
60
+ try {
61
+ const trustedUrl = new URL(trustedPattern);
62
+ const originUrl = new URL(origin);
63
+ return (trustedUrl.protocol === originUrl.protocol &&
64
+ trustedUrl.hostname === originUrl.hostname &&
65
+ trustedUrl.port === originUrl.port);
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ });
58
71
  }
@@ -11,12 +11,14 @@ const corsError_1 = require("../errors/corsError");
11
11
  const logger_1 = require("../config/logger");
12
12
  const beavuckTimeServerError_1 = require("../errors/beavuckTimeServerError");
13
13
  const corsOptions_1 = require("../config/corsOptions");
14
+ const stringUtil_1 = require("../utils/stringUtil");
14
15
  const corsMiddleware = (req, res, next) => {
15
16
  const HOST_URL = (0, urlUtil_1.tryParseUrl)(process.env.BEAVUCK_TIME_HOST_URL ?? '');
17
+ const origin = req.headers.origin;
16
18
  // 'referer' is a misspelling that was kept for compatibility: https://en.wikipedia.org/wiki/HTTP_referer
17
19
  const referrerHeader = req.headers.referrer || req.headers.referer;
18
- if (req.headers.origin) {
19
- logger_1.logger.debug(`CORS request from ${req.headers.origin}`);
20
+ if (origin) {
21
+ logger_1.logger.debug(`CORS request from ${(0, stringUtil_1.sanitizeForLog)(origin)}`);
20
22
  (0, cors_1.default)(corsOptions_1.corsOptions)(req, res, next);
21
23
  }
22
24
  else if (!HOST_URL) {
@@ -26,8 +28,8 @@ const corsMiddleware = (req, res, next) => {
26
28
  next();
27
29
  }
28
30
  else {
29
- const badOriginAndOrReferrer = `origin: ${req.headers.origin}, referrer: ${referrerHeader}`;
30
- next(new corsError_1.CorsError(badOriginAndOrReferrer));
31
+ const sanitizedReferrer = referrerHeader === undefined ? undefined : (0, stringUtil_1.sanitizeForLog)(referrerHeader);
32
+ next(new corsError_1.CorsError(`origin: ${origin}, referrer: ${sanitizedReferrer}`));
31
33
  }
32
34
  };
33
35
  exports.corsMiddleware = corsMiddleware;
@@ -38,9 +38,14 @@ function handleBeavuckClientError(res, err) {
38
38
  }
39
39
  function handleBeavuckServerError(res, err) {
40
40
  logger_1.logger.error(err);
41
- sendErrorResponse(res, err);
41
+ res.status(err.code).json({ message: beavuckTimeServerError_1.BeavuckTimeServerError.baseMessage });
42
42
  }
43
43
  function handleOtherError(res, logMsg = `Unknown error`, resStatus = http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR, resMsg = new errorResponse_1.ErrorResponse()) {
44
- logger_1.logger.error(`${logMsg}: ${JSON.stringify(resMsg)}`);
45
- res.status(resStatus).json(resMsg);
44
+ if (resStatus === http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR) {
45
+ handleBeavuckServerError(res, new beavuckTimeServerError_1.BeavuckTimeServerError(logMsg));
46
+ }
47
+ else {
48
+ logger_1.logger.error(`${logMsg}: ${JSON.stringify(resMsg)}`);
49
+ res.status(resStatus).json(resMsg);
50
+ }
46
51
  }
package/build/server.js CHANGED
@@ -22,10 +22,18 @@ function startServer(options = {}) {
22
22
  | |_) | __/ (_| |\\ V /| |_| | (__| < ,==.," , , \\,===""
23
23
  |_.__/ \\___|\\__,_| \\_/ \\__,_|\\___|_|\\_\\ < ,==) \\"'"=._.==) \\
24
24
  \`=='' \`" \`"
25
+ 888 d8b
26
+ 888 Y8P
27
+ 888
28
+ 88888888888888b.d88b. .d88b.
29
+ 888 888888 "888 "88bd8P Y8b
30
+ 888 888888 888 88888888888
31
+ Y88b. 888888 888 888Y8b.
32
+ "Y888888888 888 888 "Y8888
33
+
34
+ Beavuck Time started successfully
25
35
 
26
- Beavuck Time microservice started successfully
27
-
28
- Ready on API port ${process.env.BEAVUCK_TIME_API_PORT} (if this is running in a container, this port number is internal to the container)
36
+ Ready on port ${process.env.BEAVUCK_TIME_API_PORT} (if this is running in a container, this port number is internal to the container)
29
37
  `);
30
38
  });
31
39
  server.on('error', (err) => {
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ // src/utils/stringUtil.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.sanitizeForLog = sanitizeForLog;
5
+ function sanitizeForLog(value) {
6
+ // About the warning suppression: we're specifically working on control characters here,
7
+ // to sanitize strings for protection against log injection
8
+ // eslint-disable-next-line no-control-regex
9
+ return value.replaceAll(/[\u0000-\u001F\u007F-\u009F]/g, '');
10
+ }
@@ -5,8 +5,9 @@ exports.isSameOrigin = isSameOrigin;
5
5
  exports.tryParseUrl = tryParseUrl;
6
6
  const node_url_1 = require("node:url");
7
7
  const logger_1 = require("../config/logger");
8
+ const stringUtil_1 = require("./stringUtil");
8
9
  function isSameOrigin(thisUrl, thatUrl) {
9
- logger_1.logger.debug(`Comparing origins: ${thisUrl?.origin} and ${thatUrl?.origin}`);
10
+ logger_1.logger.debug(`Comparing origins: ${thisUrl?.origin ? (0, stringUtil_1.sanitizeForLog)(thisUrl?.origin) : undefined} and ${thatUrl?.origin ? (0, stringUtil_1.sanitizeForLog)(thatUrl?.origin) : undefined}`);
10
11
  if (!thisUrl || !thatUrl) {
11
12
  return false;
12
13
  }
@@ -27,7 +28,7 @@ function tryParseUrl(urlString) {
27
28
  return new node_url_1.URL(urlString);
28
29
  }
29
30
  catch (error) {
30
- logger_1.logger.warn(`Invalid URL string: ${urlString} (${error})`);
31
+ logger_1.logger.warn(`Invalid URL string: ${(0, stringUtil_1.sanitizeForLog)(urlString)} (${error})`);
31
32
  }
32
33
  return undefined;
33
34
  }
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "info": {
34
34
  "title": "beavuck-time",
35
- "version": "3.0.3",
35
+ "version": "3.1.1",
36
36
  "description": "Get time in ISO format, in UTC timezone, from a simple, lightweight node server",
37
37
  "license": {
38
38
  "name": "Unlicense"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beavuck-time",
3
- "version": "3.0.3",
3
+ "version": "3.1.1",
4
4
  "description": "Get time in ISO format, in UTC timezone, from a simple, lightweight node server",
5
5
  "keywords": [
6
6
  "time",
@@ -33,28 +33,32 @@
33
33
  "beavuck-time": "build/server.js"
34
34
  },
35
35
  "scripts": {
36
- "run-integration-tests": "cd api-tests/bruno && npx --yes @usebruno/cli run --env local --bail",
36
+ "run-integration-tests": "cd api_tests/time_api_tests && npx --yes @usebruno/cli run --env local --bail",
37
37
  "up-patch": "npm version patch --no-git-tag-version",
38
38
  "up-minor": "npm version minor --no-git-tag-version",
39
39
  "up-major": "npm version major --no-git-tag-version",
40
- "upgrade-dependencies": "ncu --format group && ncu -u && npm install --verbose && npm prune --verbose && npm audit fix --verbose",
41
- "prepublishOnly": "npm install && npm run build",
42
- "lint": "eslint --fix src/",
40
+ "upgrade-dependencies": "ncu --cooldown=7d --format group && ncu -u && npm run safe-install && npm prune --verbose && npm run audit",
41
+ "upgrade-dependencies:ci": "ncu --cooldown=7d --format group && ncu -u && npm run safe-install:ci && npm prune --verbose && npm run audit",
42
+ "audit": "npm audit fix --verbose",
43
+ "prepublishOnly": "npm run safe-install && npm run build",
44
+ "safe-install": "npm install --ignore-scripts --allow-git=none --min-release-age=7",
45
+ "safe-install:ci": "npm ci --ignore-scripts --allow-git=none --min-release-age=7",
46
+ "lint": "eslint --fix --cache --cache-location node_modules/.cache/.eslint_cache --report-unused-disable-directives --ignore-pattern .gitignore src/",
43
47
  "typecheck": "tsc --noEmit",
44
- "format": "prettier --write src/ --log-level warn",
48
+ "format": "prettier --write --log-level warn --cache --cache-location node_modules/.cache/.prettier_cache src/",
45
49
  "clean": "npm run lint && npm run typecheck && npm run format",
46
50
  "test": "node --env-file=.env.test node_modules/.bin/jest --coverage",
47
51
  "build": "tsoa spec-and-routes && tsc",
48
52
  "start": "node build/server.js",
49
- "go": "rm -rf build && npm install && npm run build && npm run clean && npm run test && npm run start"
53
+ "go": "rm -rf build && npm run safe-install && npm run build && npm run clean && npm run test && npm run start"
50
54
  },
51
55
  "dependencies": {
52
56
  "@tsoa/runtime": "^6.6.0",
53
57
  "cors": "^2.8.6",
54
58
  "express": "^5.2.1",
55
- "express-rate-limit": "^8.3.0",
59
+ "express-rate-limit": "^8.3.2",
56
60
  "http-status-codes": "^2.3.0",
57
- "joi": "^18.0.2",
61
+ "joi": "^18.1.2",
58
62
  "tsoa": "^6.6.0",
59
63
  "winston": "^3.19.0",
60
64
  "winston-daily-rotate-file": "^5.0.0"
@@ -64,19 +68,18 @@
64
68
  "@types/cors": "^2.8.19",
65
69
  "@types/express": "^5.0.6",
66
70
  "@types/jest": "^30.0.0",
67
- "@types/node": "^25.3.5",
71
+ "@types/node": "^25.5.2",
68
72
  "@types/supertest": "^7.2.0",
69
- "eslint": "^10.0.3",
73
+ "eslint": "^10.2.0",
70
74
  "globals": "^17.4.0",
71
- "jest": "^30.2.0",
72
- "jest-util": "^30.2.0",
73
- "knip": "^5.85.0",
74
- "npm-check-updates": "^19.6.3",
75
+ "jest": "^30.3.0",
76
+ "jest-util": "^30.3.0",
77
+ "npm-check-updates": "^20.0.0",
75
78
  "prettier": "3.8.1",
76
79
  "supertest": "^7.2.2",
77
- "ts-jest": "^29.4.6",
78
- "typescript": "^5.9.3",
79
- "typescript-eslint": "^8.56.1"
80
+ "ts-jest": "^29.4.9",
81
+ "typescript": "^6.0.2",
82
+ "typescript-eslint": "^8.58.0"
80
83
  },
81
84
  "engines": {
82
85
  "node": ">=20.12.0"