@devopsplaybook.io/common-utils 1.10.1-beta.23.b1906d7 → 1.11.0-beta.24.d8f9aef

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 (69) hide show
  1. package/README.md +73 -27
  2. package/dist/src/ConfigBase.d.ts +31 -0
  3. package/dist/src/ConfigBase.js +58 -16
  4. package/dist/src/DbUtils.d.ts +20 -3
  5. package/dist/src/DbUtils.js +93 -2
  6. package/dist/src/DbUtilsNoTelemetry.d.ts +4 -1
  7. package/dist/src/DbUtilsNoTelemetry.js +62 -6
  8. package/dist/src/PostgresDbUtils.d.ts +41 -10
  9. package/dist/src/PostgresDbUtils.js +357 -304
  10. package/dist/src/SqlDbUtils.d.ts +12 -4
  11. package/dist/src/SqlDbUtils.js +76 -30
  12. package/dist/src/users/Auth.d.ts +11 -1
  13. package/dist/src/users/Auth.js +160 -44
  14. package/dist/src/users/User.d.ts +10 -0
  15. package/dist/src/users/User.js +30 -10
  16. package/dist/src/users/UserApiToken.d.ts +4 -0
  17. package/dist/src/users/UserApiToken.js +11 -0
  18. package/dist/src/users/UsersApiTokensData.d.ts +12 -0
  19. package/dist/src/users/UsersApiTokensData.js +130 -33
  20. package/dist/src/users/UsersData.d.ts +20 -1
  21. package/dist/src/users/UsersData.js +150 -52
  22. package/dist/src/users/UsersRoutes.js +178 -61
  23. package/dist/src/users/index.d.ts +8 -0
  24. package/dist/src/users/index.js +24 -0
  25. package/package.json +58 -1
  26. package/.github/workflows/main-build.yml +0 -18
  27. package/.github/workflows/pr-check.yml +0 -27
  28. package/.github/workflows/reusable-merge-build.yml +0 -197
  29. package/.github/workflows/reusable-npm-merge.yml +0 -135
  30. package/.github/workflows/reusable-npm-pr.yml +0 -183
  31. package/.github/workflows/reusable-npm-upgrade.yml +0 -92
  32. package/.github/workflows/reusable-pr-verify.yml +0 -181
  33. package/AGENTS.md +0 -105
  34. package/index.ts +0 -18
  35. package/jest.config.js +0 -17
  36. package/prettierrc.json +0 -5
  37. package/src/ConfigBase.spec.ts +0 -108
  38. package/src/ConfigBase.ts +0 -297
  39. package/src/DbUtils.spec.ts +0 -23
  40. package/src/DbUtils.ts +0 -116
  41. package/src/DbUtilsNoTelemetry.spec.ts +0 -168
  42. package/src/DbUtilsNoTelemetry.ts +0 -117
  43. package/src/LLM.spec.ts +0 -303
  44. package/src/LLM.ts +0 -204
  45. package/src/Notifications.spec.ts +0 -265
  46. package/src/Notifications.ts +0 -201
  47. package/src/OTelContext.spec.ts +0 -58
  48. package/src/OTelContext.ts +0 -63
  49. package/src/PostgresDbUtils.spec.ts +0 -153
  50. package/src/PostgresDbUtils.ts +0 -666
  51. package/src/SqlDbUtils.spec.ts +0 -108
  52. package/src/SqlDbUtils.ts +0 -152
  53. package/src/SystemCommand.spec.ts +0 -18
  54. package/src/SystemCommand.ts +0 -23
  55. package/src/Timeout.spec.ts +0 -18
  56. package/src/Timeout.ts +0 -12
  57. package/src/users/Auth.spec.ts +0 -268
  58. package/src/users/Auth.ts +0 -202
  59. package/src/users/User.ts +0 -75
  60. package/src/users/UserApiToken.ts +0 -55
  61. package/src/users/UserPassword.spec.ts +0 -28
  62. package/src/users/UserPassword.ts +0 -20
  63. package/src/users/UserSession.ts +0 -9
  64. package/src/users/UsersApiTokensData.spec.ts +0 -158
  65. package/src/users/UsersApiTokensData.ts +0 -125
  66. package/src/users/UsersData.ts +0 -141
  67. package/src/users/UsersRoutes.ts +0 -374
  68. package/tsconfig.json +0 -15
  69. package/tsconfig.spec.json +0 -8
@@ -1,141 +0,0 @@
1
- import { StandardTracer } from "@devopsplaybook.io/otel-utils";
2
- import { Span } from "@opentelemetry/sdk-trace-base";
3
- import { DbUtilsExecSQL, DbUtilsQuerySQL } from "../DbUtils";
4
- import { User } from "./User";
5
-
6
- let tracer: StandardTracer;
7
-
8
- /**
9
- * Injects the OTel tracer instance used by the users data module.
10
- * Must be called once at startup, before any `UsersData*` function.
11
- */
12
- export function UsersDataSetOTel(tracerIn: StandardTracer): void {
13
- tracer = tracerIn;
14
- }
15
-
16
- export async function UsersDataGet(
17
- context: Span | undefined,
18
- id: string,
19
- ): Promise<User | null> {
20
- const span = tracer.startSpan("UsersDataGet", context);
21
- const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_USER_BY_ID, [
22
- id,
23
- ]);
24
- let user: User | null = null;
25
- if (usersRaw.length > 0) {
26
- user = fromRaw(usersRaw[0]);
27
- }
28
- span.end();
29
- return user;
30
- }
31
-
32
- export async function UsersDataGetByName(
33
- context: Span | undefined,
34
- name: string,
35
- ): Promise<User | null> {
36
- const span = tracer.startSpan("UsersDataGetByName", context);
37
- const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_USER_BY_NAME, [
38
- name,
39
- ]);
40
- let user: User | null = null;
41
- if (usersRaw.length > 0) {
42
- user = fromRaw(usersRaw[0]);
43
- }
44
- span.end();
45
- return user;
46
- }
47
-
48
- export async function UsersDataList(
49
- context: Span | undefined,
50
- ): Promise<User[]> {
51
- const span = tracer.startSpan("UsersDataList", context);
52
- const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.LIST_USERS);
53
- const users: User[] = [];
54
- for (const userRaw of usersRaw) {
55
- users.push(fromRaw(userRaw));
56
- }
57
- span.end();
58
- return users;
59
- }
60
-
61
- export async function UsersDataAdd(
62
- context: Span | undefined,
63
- user: User,
64
- ): Promise<void> {
65
- const span = tracer.startSpan("UsersDataAdd", context);
66
- await DbUtilsExecSQL(span, SQL_QUERIES.INSERT_USER, [
67
- user.id,
68
- user.name,
69
- user.passwordEncrypted,
70
- user.role,
71
- JSON.stringify(user.scopes),
72
- ]);
73
- span.end();
74
- }
75
-
76
- export async function UsersDataUpdatePassword(
77
- context: Span | undefined,
78
- user: User,
79
- ): Promise<void> {
80
- const span = tracer.startSpan("UsersDataUpdatePassword", context);
81
- await DbUtilsExecSQL(span, SQL_QUERIES.UPDATE_PASSWORD, [
82
- user.passwordEncrypted,
83
- user.id,
84
- ]);
85
- span.end();
86
- }
87
-
88
- export async function UsersDataUpdateUser(
89
- context: Span | undefined,
90
- user: User,
91
- ): Promise<void> {
92
- const span = tracer.startSpan("UsersDataUpdateUser", context);
93
- await DbUtilsExecSQL(span, SQL_QUERIES.UPDATE_USER, [
94
- user.role,
95
- JSON.stringify(user.scopes),
96
- user.id,
97
- ]);
98
- span.end();
99
- }
100
-
101
- export async function UsersDataDelete(
102
- context: Span | undefined,
103
- id: string,
104
- ): Promise<void> {
105
- const span = tracer.startSpan("UsersDataDelete", context);
106
- await DbUtilsExecSQL(span, SQL_QUERIES.DELETE_USER, [id]);
107
- span.end();
108
- }
109
-
110
- // Private Functions
111
-
112
- function fromRaw(userRaw: any): User {
113
- const user = new User();
114
- user.id = userRaw.id;
115
- user.name = userRaw.name;
116
- user.passwordEncrypted = userRaw.passwordEncrypted;
117
- user.role = userRaw.role || "user";
118
- if (userRaw.scopes) {
119
- try {
120
- user.scopes = JSON.parse(userRaw.scopes);
121
- } catch {
122
- user.scopes = [...User.DEFAULT_SCOPES];
123
- }
124
- }
125
- return user;
126
- }
127
-
128
- // SQL
129
- // Written SQLite-first with quoted identifiers (valid for both backends);
130
- // the DbUtils facade converts `?` placeholders for Postgres.
131
-
132
- const SQL_QUERIES = {
133
- GET_USER_BY_ID: 'SELECT * FROM users WHERE "id" = ?',
134
- GET_USER_BY_NAME: 'SELECT * FROM users WHERE "name" = ?',
135
- LIST_USERS: "SELECT * FROM users",
136
- INSERT_USER:
137
- 'INSERT INTO users ("id", "name", "passwordEncrypted", "role", "scopes") VALUES (?, ?, ?, ?, ?)',
138
- UPDATE_USER: 'UPDATE users SET "role" = ?, "scopes" = ? WHERE "id" = ?',
139
- UPDATE_PASSWORD: 'UPDATE users SET "passwordEncrypted" = ? WHERE "id" = ?',
140
- DELETE_USER: 'DELETE FROM users WHERE "id" = ?',
141
- };
@@ -1,374 +0,0 @@
1
- import { createHash } from "crypto";
2
- import { Span } from "@opentelemetry/sdk-trace-base";
3
- import { FastifyInstance, RequestGenericInterface } from "fastify";
4
- import { v4 as uuidv4 } from "uuid";
5
- import { AuthGenerateJWT, AuthGetUserSession, AuthMustBeAdmin } from "./Auth";
6
- import { User } from "./User";
7
- import { UserApiToken } from "./UserApiToken";
8
- import {
9
- UserPasswordCheckPassword,
10
- UserPasswordSetPassword,
11
- } from "./UserPassword";
12
- import {
13
- UsersDataAdd,
14
- UsersDataDelete,
15
- UsersDataGet,
16
- UsersDataGetByName,
17
- UsersDataList,
18
- UsersDataUpdatePassword,
19
- UsersDataUpdateUser,
20
- } from "./UsersData";
21
- import {
22
- UsersApiTokensDataAdd,
23
- UsersApiTokensDataDelete,
24
- UsersApiTokensDataDeleteByUser,
25
- UsersApiTokensDataGet,
26
- UsersApiTokensDataListByUser,
27
- } from "./UsersApiTokensData";
28
-
29
- /**
30
- * Retrieves the OTel span attached to the request by the
31
- * `@devopsplaybook.io/otel-utils-fastify` hooks.
32
- */
33
- function requestSpan(req: any): Span | undefined {
34
- return req?.tracerSpanApi;
35
- }
36
-
37
- /**
38
- * Standard user management routes shared across applications:
39
- * initialization status, login (session), user CRUD and password changes.
40
- *
41
- * Register on a fastify instance:
42
- * ```ts
43
- * fastify.register(new UsersRoutes().getRoutes, { prefix: "/api/users" });
44
- * ```
45
- */
46
- export class UsersRoutes {
47
- //
48
-
49
- public async getRoutes(fastify: FastifyInstance): Promise<void> {
50
- //
51
- fastify.get("/status/initialization", async (req, res) => {
52
- if ((await UsersDataList(requestSpan(req))).length === 0) {
53
- res.status(201).send({ initialized: false });
54
- } else {
55
- res.status(201).send({ initialized: true });
56
- }
57
- });
58
-
59
- // ==================== SESSION (Login) ====================
60
-
61
- interface PostSession extends RequestGenericInterface {
62
- Body: {
63
- name: string;
64
- password: string;
65
- };
66
- }
67
- fastify.post<PostSession>("/session", async (req, res) => {
68
- let user: User | null;
69
- // From token
70
- const userSession = await AuthGetUserSession(req);
71
- if (userSession.isAuthenticated) {
72
- // isAuthenticated implies userId is set
73
- user = await UsersDataGet(
74
- requestSpan(req),
75
- userSession.userId as string,
76
- );
77
- if (!user) {
78
- return res.status(403).send({ error: "Authentication Failed" });
79
- }
80
- return res.status(201).send({
81
- success: true,
82
- token: await AuthGenerateJWT(user),
83
- user: user.toTransportJson(),
84
- });
85
- }
86
-
87
- // From User/Pass
88
- if (!req.body.name) {
89
- return res.status(400).send({ error: "Missing: Name" });
90
- }
91
- if (!req.body.password) {
92
- return res.status(400).send({ error: "Missing: Password" });
93
- }
94
- user = await UsersDataGetByName(requestSpan(req), req.body.name);
95
- if (!user) {
96
- return res.status(403).send({ error: "Authentication Failed" });
97
- } else if (
98
- await UserPasswordCheckPassword(
99
- requestSpan(req),
100
- user,
101
- req.body.password,
102
- )
103
- ) {
104
- return res.status(201).send({
105
- success: true,
106
- token: await AuthGenerateJWT(user),
107
- user: user.toTransportJson(),
108
- });
109
- } else {
110
- return res.status(403).send({ error: "Authentication Failed" });
111
- }
112
- });
113
-
114
- // ==================== LIST USERS (Admin only) ====================
115
-
116
- fastify.get("/", async (req, res) => {
117
- try {
118
- await AuthMustBeAdmin(req, res);
119
- } catch {
120
- return;
121
- }
122
- const users = await UsersDataList(requestSpan(req));
123
- return res.status(200).send(users.map((u) => u.toTransportJson()));
124
- });
125
-
126
- // ==================== CREATE USER ====================
127
-
128
- interface PostUser extends RequestGenericInterface {
129
- Body: {
130
- name: string;
131
- password: string;
132
- role?: string;
133
- scopes?: string[];
134
- };
135
- }
136
- fastify.post<PostUser>("/", async (req, res) => {
137
- const context = requestSpan(req);
138
- let isInitialized = true;
139
- if ((await UsersDataList(context)).length === 0) {
140
- isInitialized = false;
141
- }
142
-
143
- // If initialized, only admin can create users
144
- if (isInitialized) {
145
- try {
146
- await AuthMustBeAdmin(req, res);
147
- } catch {
148
- return;
149
- }
150
- }
151
-
152
- if (!req.body.name) {
153
- return res.status(400).send({ error: "Missing: Name" });
154
- }
155
- if (!req.body.password) {
156
- return res.status(400).send({ error: "Missing: Password" });
157
- }
158
- if (await UsersDataGetByName(context, req.body.name)) {
159
- return res.status(400).send({ error: "Username Already Exists" });
160
- }
161
-
162
- const newUser = new User();
163
- newUser.name = req.body.name;
164
- // First user is always admin
165
- if (isInitialized) {
166
- newUser.role = req.body.role === "admin" ? "admin" : "user";
167
- if (req.body.scopes && Array.isArray(req.body.scopes)) {
168
- newUser.scopes = req.body.scopes.filter((s) =>
169
- User.ALL_SCOPES.includes(s),
170
- );
171
- }
172
- } else {
173
- newUser.role = "admin";
174
- newUser.scopes = [...User.ALL_SCOPES];
175
- }
176
- await UserPasswordSetPassword(context, newUser, req.body.password);
177
- await UsersDataAdd(context, newUser);
178
- res.status(201).send({ user: newUser.toTransportJson() });
179
- });
180
-
181
- // ==================== ADMIN: CHANGE OWN PASSWORD ====================
182
-
183
- interface PutOwnPassword extends RequestGenericInterface {
184
- Body: {
185
- password: string;
186
- passwordOld: string;
187
- };
188
- }
189
- fastify.put<PutOwnPassword>("/password", async (req, res) => {
190
- const context = requestSpan(req);
191
- const userSession = await AuthGetUserSession(req);
192
- if (!userSession.isAuthenticated) {
193
- return res.status(403).send({ error: "Access Denied" });
194
- }
195
- // isAuthenticated implies userId is set
196
- const user = await UsersDataGet(context, userSession.userId as string);
197
- if (!user) {
198
- return res.status(403).send({ error: "Access Denied" });
199
- }
200
- if (!req.body.password) {
201
- return res.status(400).send({ error: "Missing: Password" });
202
- }
203
- if (
204
- !(await UserPasswordCheckPassword(context, user, req.body.passwordOld))
205
- ) {
206
- return res.status(403).send({ error: "Old Password Wrong" });
207
- }
208
- await UserPasswordSetPassword(context, user, req.body.password);
209
- await UsersDataUpdatePassword(context, user);
210
- res.status(201).send({});
211
- });
212
-
213
- // ==================== ADMIN: UPDATE USER (role, scopes, password) ====================
214
-
215
- interface PutUser extends RequestGenericInterface {
216
- Params: {
217
- id: string;
218
- };
219
- Body: {
220
- role?: string;
221
- scopes?: string[];
222
- password?: string;
223
- };
224
- }
225
- fastify.put<PutUser>("/:id", async (req, res) => {
226
- const context = requestSpan(req);
227
- try {
228
- await AuthMustBeAdmin(req, res);
229
- } catch {
230
- return;
231
- }
232
-
233
- const user = await UsersDataGet(context, req.params.id);
234
- if (!user) {
235
- return res.status(404).send({ error: "User Not Found" });
236
- }
237
-
238
- if (req.body.role) {
239
- user.role = req.body.role === "admin" ? "admin" : "user";
240
- }
241
- if (req.body.scopes && Array.isArray(req.body.scopes)) {
242
- user.scopes = req.body.scopes.filter((s) =>
243
- User.ALL_SCOPES.includes(s),
244
- );
245
- }
246
-
247
- await UsersDataUpdateUser(context, user);
248
-
249
- // If password change requested
250
- if (req.body.password) {
251
- await UserPasswordSetPassword(context, user, req.body.password);
252
- await UsersDataUpdatePassword(context, user);
253
- }
254
-
255
- res.status(201).send({ user: user.toTransportJson() });
256
- });
257
-
258
- // ==================== ADMIN: DELETE USER ====================
259
-
260
- interface DeleteUser extends RequestGenericInterface {
261
- Params: {
262
- id: string;
263
- };
264
- }
265
- fastify.delete<DeleteUser>("/:id", async (req, res) => {
266
- const context = requestSpan(req);
267
- try {
268
- await AuthMustBeAdmin(req, res);
269
- } catch {
270
- return;
271
- }
272
-
273
- const userSession = await AuthGetUserSession(req);
274
-
275
- // Cannot delete yourself
276
- if (userSession.userId === req.params.id) {
277
- return res.status(400).send({ error: "Cannot Delete Yourself" });
278
- }
279
-
280
- const user = await UsersDataGet(context, req.params.id);
281
- if (!user) {
282
- return res.status(404).send({ error: "User Not Found" });
283
- }
284
-
285
- // Check that at least 1 admin remains
286
- if (user.role === "admin") {
287
- const admins = (await UsersDataList(context)).filter(
288
- (u) => u.role === "admin",
289
- );
290
- if (admins.length <= 1) {
291
- return res
292
- .status(400)
293
- .send({ error: "At least 1 admin must be defined" });
294
- }
295
- }
296
-
297
- await UsersDataDelete(context, req.params.id);
298
- await UsersApiTokensDataDeleteByUser(context, req.params.id);
299
- res.status(201).send({});
300
- });
301
-
302
- // ==================== API TOKENS (self-service) ====================
303
-
304
- interface PostApiToken extends RequestGenericInterface {
305
- Body: {
306
- name: string;
307
- };
308
- }
309
- fastify.post<PostApiToken>("/tokens", async (req, res) => {
310
- const context = requestSpan(req);
311
- const userSession = await AuthGetUserSession(req);
312
- if (!userSession.isAuthenticated) {
313
- return res.status(403).send({ error: "Access Denied" });
314
- }
315
- if (!req.body || !req.body.name) {
316
- return res.status(400).send({ error: "Missing: Name" });
317
- }
318
- const apiToken = new UserApiToken();
319
- apiToken.name = req.body.name;
320
- // isAuthenticated implies userId is set
321
- apiToken.userId = userSession.userId as string;
322
- apiToken.dateCreated = new Date().toISOString();
323
- const token = uuidv4();
324
- apiToken.tokenHash = createHash("sha256").update(token).digest("hex");
325
- await UsersApiTokensDataAdd(context, apiToken);
326
- // The plaintext token is returned once; only its hash is stored.
327
- return res.status(201).send({
328
- token,
329
- ...apiToken.toTransportJson(),
330
- });
331
- });
332
-
333
- fastify.get("/tokens", async (req, res) => {
334
- const context = requestSpan(req);
335
- const userSession = await AuthGetUserSession(req);
336
- if (!userSession.isAuthenticated) {
337
- return res.status(403).send({ error: "Access Denied" });
338
- }
339
- const apiTokens = await UsersApiTokensDataListByUser(
340
- context,
341
- userSession.userId as string,
342
- );
343
- return res
344
- .status(200)
345
- .send(apiTokens.map((t) => t.toTransportJson()));
346
- });
347
-
348
- interface DeleteApiToken extends RequestGenericInterface {
349
- Params: {
350
- id: string;
351
- };
352
- }
353
- fastify.delete<DeleteApiToken>("/tokens/:id", async (req, res) => {
354
- const context = requestSpan(req);
355
- const userSession = await AuthGetUserSession(req);
356
- if (!userSession.isAuthenticated) {
357
- return res.status(403).send({ error: "Access Denied" });
358
- }
359
- const apiToken = await UsersApiTokensDataGet(context, req.params.id);
360
- if (!apiToken) {
361
- return res.status(404).send({ error: "API Token Not Found" });
362
- }
363
- // Owner can always revoke; admins can revoke any token
364
- if (
365
- apiToken.userId !== userSession.userId &&
366
- userSession.role !== "admin"
367
- ) {
368
- return res.status(403).send({ error: "Access Denied" });
369
- }
370
- await UsersApiTokensDataDelete(context, apiToken.id);
371
- return res.status(201).send({});
372
- });
373
- }
374
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2019",
4
- "lib": ["ES2019", "ES2022.Error"],
5
- "module": "commonjs",
6
- "declaration": true,
7
- "outDir": "./dist",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": ["index.ts", "src/**/*"],
14
- "exclude": ["**/*.spec.ts"]
15
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "types": ["jest", "node"]
5
- },
6
- "include": ["src/**/*.spec.ts"],
7
- "exclude": []
8
- }