@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
@@ -6,9 +6,21 @@ import { UserApiToken } from "./UserApiToken";
6
6
  * Must be called once at startup, before any `UsersApiTokensData*` function.
7
7
  */
8
8
  export declare function UsersApiTokensDataSetOTel(tracerIn: StandardTracer): void;
9
+ /**
10
+ * One-time capability probe for the optional `users_api_tokens` columns
11
+ * (`expiresAt`, `lastUsedAt`) added by the documented migration.
12
+ */
13
+ export declare function UsersApiTokensDataSupportsOptionalColumns(context: Span | undefined): Promise<boolean>;
9
14
  export declare function UsersApiTokensDataGet(context: Span | undefined, id: string): Promise<UserApiToken | null>;
10
15
  export declare function UsersApiTokensDataGetByTokenHash(context: Span | undefined, tokenHash: string): Promise<UserApiToken | null>;
11
16
  export declare function UsersApiTokensDataListByUser(context: Span | undefined, userId: string): Promise<UserApiToken[]>;
17
+ /** Number of tokens owned by a user (per-user cap check). */
18
+ export declare function UsersApiTokensDataCountByUser(context: Span | undefined, userId: string): Promise<number>;
12
19
  export declare function UsersApiTokensDataAdd(context: Span | undefined, apiToken: UserApiToken): Promise<void>;
13
20
  export declare function UsersApiTokensDataDelete(context: Span | undefined, id: string): Promise<void>;
14
21
  export declare function UsersApiTokensDataDeleteByUser(context: Span | undefined, userId: string): Promise<void>;
22
+ /**
23
+ * Record a token's last successful use. Best effort: only attempted when the
24
+ * optional columns exist, and failures never break authentication.
25
+ */
26
+ export declare function UsersApiTokensDataSetLastUsed(context: Span | undefined, id: string, lastUsedAt: string): Promise<void>;
@@ -1,15 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UsersApiTokensDataSetOTel = UsersApiTokensDataSetOTel;
4
+ exports.UsersApiTokensDataSupportsOptionalColumns = UsersApiTokensDataSupportsOptionalColumns;
4
5
  exports.UsersApiTokensDataGet = UsersApiTokensDataGet;
5
6
  exports.UsersApiTokensDataGetByTokenHash = UsersApiTokensDataGetByTokenHash;
6
7
  exports.UsersApiTokensDataListByUser = UsersApiTokensDataListByUser;
8
+ exports.UsersApiTokensDataCountByUser = UsersApiTokensDataCountByUser;
7
9
  exports.UsersApiTokensDataAdd = UsersApiTokensDataAdd;
8
10
  exports.UsersApiTokensDataDelete = UsersApiTokensDataDelete;
9
11
  exports.UsersApiTokensDataDeleteByUser = UsersApiTokensDataDeleteByUser;
12
+ exports.UsersApiTokensDataSetLastUsed = UsersApiTokensDataSetLastUsed;
10
13
  const DbUtils_1 = require("../DbUtils");
11
14
  const UserApiToken_1 = require("./UserApiToken");
12
15
  let tracer;
16
+ /**
17
+ * Whether the optional `expiresAt` / `lastUsedAt` columns exist. Probed once
18
+ * per process so a database that did not run the documented migration never
19
+ * breaks the auth path.
20
+ */
21
+ let optionalColumnsAvailable = null;
13
22
  /**
14
23
  * Injects the OTel tracer instance used by the API tokens data module.
15
24
  * Must be called once at startup, before any `UsersApiTokensData*` function.
@@ -17,67 +26,152 @@ let tracer;
17
26
  function UsersApiTokensDataSetOTel(tracerIn) {
18
27
  tracer = tracerIn;
19
28
  }
29
+ /**
30
+ * One-time capability probe for the optional `users_api_tokens` columns
31
+ * (`expiresAt`, `lastUsedAt`) added by the documented migration.
32
+ */
33
+ async function UsersApiTokensDataSupportsOptionalColumns(context) {
34
+ if (optionalColumnsAvailable === null) {
35
+ try {
36
+ await (0, DbUtils_1.DbUtilsQuerySQL)(context, 'SELECT "expiresAt" FROM users_api_tokens LIMIT 1');
37
+ optionalColumnsAvailable = true;
38
+ }
39
+ catch {
40
+ optionalColumnsAvailable = false;
41
+ }
42
+ }
43
+ return optionalColumnsAvailable;
44
+ }
20
45
  async function UsersApiTokensDataGet(context, id) {
21
46
  const span = tracer.startSpan("UsersApiTokensDataGet", context);
22
- const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_TOKEN_BY_ID, [
23
- id,
24
- ]);
25
- let apiToken = null;
26
- if (tokensRaw.length > 0) {
27
- apiToken = fromRaw(tokensRaw[0]);
28
- }
29
- span.end();
30
- return apiToken;
47
+ try {
48
+ const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_TOKEN_BY_ID, [
49
+ id,
50
+ ]);
51
+ if (tokensRaw.length > 0) {
52
+ return fromRaw(tokensRaw[0]);
53
+ }
54
+ return null;
55
+ }
56
+ finally {
57
+ span.end();
58
+ }
31
59
  }
32
60
  async function UsersApiTokensDataGetByTokenHash(context, tokenHash) {
33
61
  const span = tracer.startSpan("UsersApiTokensDataGetByTokenHash", context);
34
- const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_TOKEN_BY_HASH, [tokenHash]);
35
- let apiToken = null;
36
- if (tokensRaw.length > 0) {
37
- apiToken = fromRaw(tokensRaw[0]);
62
+ try {
63
+ const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_TOKEN_BY_HASH, [tokenHash]);
64
+ if (tokensRaw.length > 0) {
65
+ return fromRaw(tokensRaw[0]);
66
+ }
67
+ return null;
68
+ }
69
+ finally {
70
+ span.end();
38
71
  }
39
- span.end();
40
- return apiToken;
41
72
  }
42
73
  async function UsersApiTokensDataListByUser(context, userId) {
43
74
  const span = tracer.startSpan("UsersApiTokensDataListByUser", context);
44
- const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.LIST_TOKENS_BY_USER, [userId]);
45
- const apiTokens = [];
46
- for (const tokenRaw of tokensRaw) {
47
- apiTokens.push(fromRaw(tokenRaw));
75
+ try {
76
+ const tokensRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.LIST_TOKENS_BY_USER, [userId]);
77
+ return tokensRaw.map(fromRaw);
78
+ }
79
+ finally {
80
+ span.end();
81
+ }
82
+ }
83
+ /** Number of tokens owned by a user (per-user cap check). */
84
+ async function UsersApiTokensDataCountByUser(context, userId) {
85
+ var _a;
86
+ var _b;
87
+ const span = tracer.startSpan("UsersApiTokensDataCountByUser", context);
88
+ try {
89
+ const rows = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.COUNT_TOKENS_BY_USER, [
90
+ userId,
91
+ ]);
92
+ return Number((_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0);
93
+ }
94
+ finally {
95
+ span.end();
48
96
  }
49
- span.end();
50
- return apiTokens;
51
97
  }
52
98
  async function UsersApiTokensDataAdd(context, apiToken) {
53
99
  const span = tracer.startSpan("UsersApiTokensDataAdd", context);
54
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.INSERT_TOKEN, [
55
- apiToken.id,
56
- apiToken.name,
57
- apiToken.userId,
58
- apiToken.tokenHash,
59
- apiToken.dateCreated,
60
- ]);
61
- span.end();
100
+ try {
101
+ if (apiToken.expiresAt) {
102
+ if (!(await UsersApiTokensDataSupportsOptionalColumns(context))) {
103
+ throw new Error("users_api_tokens.expiresAt does not exist: run the documented migration to enable API token expiry");
104
+ }
105
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.INSERT_TOKEN_WITH_EXPIRY, [
106
+ apiToken.id,
107
+ apiToken.name,
108
+ apiToken.userId,
109
+ apiToken.tokenHash,
110
+ apiToken.dateCreated,
111
+ apiToken.expiresAt,
112
+ ]);
113
+ return;
114
+ }
115
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.INSERT_TOKEN, [
116
+ apiToken.id,
117
+ apiToken.name,
118
+ apiToken.userId,
119
+ apiToken.tokenHash,
120
+ apiToken.dateCreated,
121
+ ]);
122
+ }
123
+ finally {
124
+ span.end();
125
+ }
62
126
  }
63
127
  async function UsersApiTokensDataDelete(context, id) {
64
128
  const span = tracer.startSpan("UsersApiTokensDataDelete", context);
65
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_TOKEN, [id]);
66
- span.end();
129
+ try {
130
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_TOKEN, [id]);
131
+ }
132
+ finally {
133
+ span.end();
134
+ }
67
135
  }
68
136
  async function UsersApiTokensDataDeleteByUser(context, userId) {
69
137
  const span = tracer.startSpan("UsersApiTokensDataDeleteByUser", context);
70
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_TOKENS_BY_USER, [userId]);
71
- span.end();
138
+ try {
139
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_TOKENS_BY_USER, [userId]);
140
+ }
141
+ finally {
142
+ span.end();
143
+ }
144
+ }
145
+ /**
146
+ * Record a token's last successful use. Best effort: only attempted when the
147
+ * optional columns exist, and failures never break authentication.
148
+ */
149
+ async function UsersApiTokensDataSetLastUsed(context, id, lastUsedAt) {
150
+ if (!(await UsersApiTokensDataSupportsOptionalColumns(context))) {
151
+ return;
152
+ }
153
+ const span = tracer.startSpan("UsersApiTokensDataSetLastUsed", context);
154
+ try {
155
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.UPDATE_TOKEN_LAST_USED, [
156
+ lastUsedAt,
157
+ id,
158
+ ]);
159
+ }
160
+ finally {
161
+ span.end();
162
+ }
72
163
  }
73
164
  // Private Functions
74
165
  function fromRaw(tokenRaw) {
166
+ var _a, _b;
75
167
  const apiToken = new UserApiToken_1.UserApiToken();
76
168
  apiToken.id = tokenRaw.id;
77
169
  apiToken.name = tokenRaw.name;
78
170
  apiToken.userId = tokenRaw.userId;
79
171
  apiToken.tokenHash = tokenRaw.tokenHash;
80
172
  apiToken.dateCreated = tokenRaw.dateCreated;
173
+ apiToken.expiresAt = (_a = tokenRaw.expiresAt) !== null && _a !== void 0 ? _a : null;
174
+ apiToken.lastUsedAt = (_b = tokenRaw.lastUsedAt) !== null && _b !== void 0 ? _b : null;
81
175
  return apiToken;
82
176
  }
83
177
  // SQL
@@ -87,7 +181,10 @@ const SQL_QUERIES = {
87
181
  GET_TOKEN_BY_ID: 'SELECT * FROM users_api_tokens WHERE "id" = ?',
88
182
  GET_TOKEN_BY_HASH: 'SELECT * FROM users_api_tokens WHERE "tokenHash" = ?',
89
183
  LIST_TOKENS_BY_USER: 'SELECT * FROM users_api_tokens WHERE "userId" = ?',
184
+ COUNT_TOKENS_BY_USER: 'SELECT COUNT(*) as count FROM users_api_tokens WHERE "userId" = ?',
90
185
  INSERT_TOKEN: 'INSERT INTO users_api_tokens ("id", "name", "userId", "tokenHash", "dateCreated") VALUES (?, ?, ?, ?, ?)',
186
+ INSERT_TOKEN_WITH_EXPIRY: 'INSERT INTO users_api_tokens ("id", "name", "userId", "tokenHash", "dateCreated", "expiresAt") VALUES (?, ?, ?, ?, ?, ?)',
187
+ UPDATE_TOKEN_LAST_USED: 'UPDATE users_api_tokens SET "lastUsedAt" = ? WHERE "id" = ?',
91
188
  DELETE_TOKEN: 'DELETE FROM users_api_tokens WHERE "id" = ?',
92
189
  DELETE_TOKENS_BY_USER: 'DELETE FROM users_api_tokens WHERE "userId" = ?',
93
190
  };
@@ -8,8 +8,27 @@ import { User } from "./User";
8
8
  export declare function UsersDataSetOTel(tracerIn: StandardTracer): void;
9
9
  export declare function UsersDataGet(context: Span | undefined, id: string): Promise<User | null>;
10
10
  export declare function UsersDataGetByName(context: Span | undefined, name: string): Promise<User | null>;
11
- export declare function UsersDataList(context: Span | undefined): Promise<User[]>;
11
+ /**
12
+ * List users, optionally paginated. `offset` is only applied together with a
13
+ * `limit` (SQLite requires a LIMIT clause for OFFSET).
14
+ */
15
+ export declare function UsersDataList(context: Span | undefined, limit?: number, offset?: number): Promise<User[]>;
16
+ /** Number of users (COUNT(*) – used instead of loading the full list). */
17
+ export declare function UsersDataCount(context: Span | undefined): Promise<number>;
18
+ /** Number of admin users (COUNT(*) – used by the last-admin guards). */
19
+ export declare function UsersDataCountAdmins(context: Span | undefined): Promise<number>;
12
20
  export declare function UsersDataAdd(context: Span | undefined, user: User): Promise<void>;
13
21
  export declare function UsersDataUpdatePassword(context: Span | undefined, user: User): Promise<void>;
14
22
  export declare function UsersDataUpdateUser(context: Span | undefined, user: User): Promise<void>;
15
23
  export declare function UsersDataDelete(context: Span | undefined, id: string): Promise<void>;
24
+ /**
25
+ * Invalidate the JWTs issued for a user by incrementing `tokenVersion`.
26
+ * Only called when `JWT_REVOCATION_ENABLED` is on: the column is part of the
27
+ * documented `users` migration and absent otherwise.
28
+ */
29
+ export declare function UsersDataBumpTokenVersion(context: Span | undefined, id: string): Promise<void>;
30
+ /**
31
+ * Whether an error is a unique-constraint violation (`SQLITE_CONSTRAINT*` /
32
+ * Postgres `23505`) – used to turn the user-name uniqueness race into a 400.
33
+ */
34
+ export declare function isUniqueViolationError(error: unknown): boolean;
@@ -4,10 +4,14 @@ exports.UsersDataSetOTel = UsersDataSetOTel;
4
4
  exports.UsersDataGet = UsersDataGet;
5
5
  exports.UsersDataGetByName = UsersDataGetByName;
6
6
  exports.UsersDataList = UsersDataList;
7
+ exports.UsersDataCount = UsersDataCount;
8
+ exports.UsersDataCountAdmins = UsersDataCountAdmins;
7
9
  exports.UsersDataAdd = UsersDataAdd;
8
10
  exports.UsersDataUpdatePassword = UsersDataUpdatePassword;
9
11
  exports.UsersDataUpdateUser = UsersDataUpdateUser;
10
12
  exports.UsersDataDelete = UsersDataDelete;
13
+ exports.UsersDataBumpTokenVersion = UsersDataBumpTokenVersion;
14
+ exports.isUniqueViolationError = isUniqueViolationError;
11
15
  const DbUtils_1 = require("../DbUtils");
12
16
  const User_1 = require("./User");
13
17
  let tracer;
@@ -20,85 +24,176 @@ function UsersDataSetOTel(tracerIn) {
20
24
  }
21
25
  async function UsersDataGet(context, id) {
22
26
  const span = tracer.startSpan("UsersDataGet", context);
23
- const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_USER_BY_ID, [
24
- id,
25
- ]);
26
- let user = null;
27
- if (usersRaw.length > 0) {
28
- user = fromRaw(usersRaw[0]);
29
- }
30
- span.end();
31
- return user;
27
+ try {
28
+ const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_USER_BY_ID, [
29
+ id,
30
+ ]);
31
+ if (usersRaw.length > 0) {
32
+ return fromRaw(usersRaw[0]);
33
+ }
34
+ return null;
35
+ }
36
+ finally {
37
+ span.end();
38
+ }
32
39
  }
33
40
  async function UsersDataGetByName(context, name) {
34
41
  const span = tracer.startSpan("UsersDataGetByName", context);
35
- const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_USER_BY_NAME, [
36
- name,
37
- ]);
38
- let user = null;
39
- if (usersRaw.length > 0) {
40
- user = fromRaw(usersRaw[0]);
41
- }
42
- span.end();
43
- return user;
42
+ try {
43
+ const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.GET_USER_BY_NAME, [
44
+ name,
45
+ ]);
46
+ if (usersRaw.length > 0) {
47
+ return fromRaw(usersRaw[0]);
48
+ }
49
+ return null;
50
+ }
51
+ finally {
52
+ span.end();
53
+ }
44
54
  }
45
- async function UsersDataList(context) {
55
+ /**
56
+ * List users, optionally paginated. `offset` is only applied together with a
57
+ * `limit` (SQLite requires a LIMIT clause for OFFSET).
58
+ */
59
+ async function UsersDataList(context, limit, offset) {
46
60
  const span = tracer.startSpan("UsersDataList", context);
47
- const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.LIST_USERS);
48
- const users = [];
49
- for (const userRaw of usersRaw) {
50
- users.push(fromRaw(userRaw));
61
+ try {
62
+ let sql = SQL_QUERIES.LIST_USERS;
63
+ const params = [];
64
+ if (limit !== undefined) {
65
+ sql += " LIMIT ?";
66
+ params.push(limit);
67
+ if (offset !== undefined) {
68
+ sql += " OFFSET ?";
69
+ params.push(offset);
70
+ }
71
+ }
72
+ const usersRaw = await (0, DbUtils_1.DbUtilsQuerySQL)(span, sql, params);
73
+ return usersRaw.map(fromRaw);
74
+ }
75
+ finally {
76
+ span.end();
77
+ }
78
+ }
79
+ /** Number of users (COUNT(*) – used instead of loading the full list). */
80
+ async function UsersDataCount(context) {
81
+ var _a;
82
+ var _b;
83
+ const span = tracer.startSpan("UsersDataCount", context);
84
+ try {
85
+ const rows = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.COUNT_USERS);
86
+ return Number((_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0);
87
+ }
88
+ finally {
89
+ span.end();
90
+ }
91
+ }
92
+ /** Number of admin users (COUNT(*) – used by the last-admin guards). */
93
+ async function UsersDataCountAdmins(context) {
94
+ var _a;
95
+ var _b;
96
+ const span = tracer.startSpan("UsersDataCountAdmins", context);
97
+ try {
98
+ const rows = await (0, DbUtils_1.DbUtilsQuerySQL)(span, SQL_QUERIES.COUNT_ADMINS);
99
+ return Number((_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0);
100
+ }
101
+ finally {
102
+ span.end();
51
103
  }
52
- span.end();
53
- return users;
54
104
  }
55
105
  async function UsersDataAdd(context, user) {
56
106
  const span = tracer.startSpan("UsersDataAdd", context);
57
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.INSERT_USER, [
58
- user.id,
59
- user.name,
60
- user.passwordEncrypted,
61
- user.role,
62
- JSON.stringify(user.scopes),
63
- ]);
64
- span.end();
107
+ try {
108
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.INSERT_USER, [
109
+ user.id,
110
+ user.name,
111
+ user.passwordEncrypted,
112
+ user.role,
113
+ JSON.stringify(user.scopes),
114
+ ]);
115
+ }
116
+ finally {
117
+ span.end();
118
+ }
65
119
  }
66
120
  async function UsersDataUpdatePassword(context, user) {
67
121
  const span = tracer.startSpan("UsersDataUpdatePassword", context);
68
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.UPDATE_PASSWORD, [
69
- user.passwordEncrypted,
70
- user.id,
71
- ]);
72
- span.end();
122
+ try {
123
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.UPDATE_PASSWORD, [
124
+ user.passwordEncrypted,
125
+ user.id,
126
+ ]);
127
+ }
128
+ finally {
129
+ span.end();
130
+ }
73
131
  }
74
132
  async function UsersDataUpdateUser(context, user) {
75
133
  const span = tracer.startSpan("UsersDataUpdateUser", context);
76
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.UPDATE_USER, [
77
- user.role,
78
- JSON.stringify(user.scopes),
79
- user.id,
80
- ]);
81
- span.end();
134
+ try {
135
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.UPDATE_USER, [
136
+ user.role,
137
+ JSON.stringify(user.scopes),
138
+ user.id,
139
+ ]);
140
+ }
141
+ finally {
142
+ span.end();
143
+ }
82
144
  }
83
145
  async function UsersDataDelete(context, id) {
84
146
  const span = tracer.startSpan("UsersDataDelete", context);
85
- await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_USER, [id]);
86
- span.end();
147
+ try {
148
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.DELETE_USER, [id]);
149
+ }
150
+ finally {
151
+ span.end();
152
+ }
153
+ }
154
+ /**
155
+ * Invalidate the JWTs issued for a user by incrementing `tokenVersion`.
156
+ * Only called when `JWT_REVOCATION_ENABLED` is on: the column is part of the
157
+ * documented `users` migration and absent otherwise.
158
+ */
159
+ async function UsersDataBumpTokenVersion(context, id) {
160
+ const span = tracer.startSpan("UsersDataBumpTokenVersion", context);
161
+ try {
162
+ await (0, DbUtils_1.DbUtilsExecSQL)(span, SQL_QUERIES.BUMP_TOKEN_VERSION, [id]);
163
+ }
164
+ finally {
165
+ span.end();
166
+ }
167
+ }
168
+ /**
169
+ * Whether an error is a unique-constraint violation (`SQLITE_CONSTRAINT*` /
170
+ * Postgres `23505`) – used to turn the user-name uniqueness race into a 400.
171
+ */
172
+ function isUniqueViolationError(error) {
173
+ if (!error) {
174
+ return false;
175
+ }
176
+ const err = error;
177
+ if (err.code === "23505" ||
178
+ err.code === "SQLITE_CONSTRAINT_UNIQUE" ||
179
+ err.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
180
+ return true;
181
+ }
182
+ const message = typeof err.message === "string" ? err.message : "";
183
+ return (/UNIQUE constraint failed/i.test(message) ||
184
+ /duplicate key value violates unique constraint/i.test(message));
87
185
  }
88
186
  // Private Functions
89
187
  function fromRaw(userRaw) {
188
+ var _a;
90
189
  const user = new User_1.User();
91
190
  user.id = userRaw.id;
92
191
  user.name = userRaw.name;
93
192
  user.passwordEncrypted = userRaw.passwordEncrypted;
94
193
  user.role = userRaw.role || "user";
194
+ user.tokenVersion = Number((_a = userRaw.tokenVersion) !== null && _a !== void 0 ? _a : 0) || 0;
95
195
  if (userRaw.scopes) {
96
- try {
97
- user.scopes = JSON.parse(userRaw.scopes);
98
- }
99
- catch {
100
- user.scopes = [...User_1.User.DEFAULT_SCOPES];
101
- }
196
+ user.scopes = User_1.User.normalizeScopes(userRaw.scopes);
102
197
  }
103
198
  return user;
104
199
  }
@@ -109,8 +204,11 @@ const SQL_QUERIES = {
109
204
  GET_USER_BY_ID: 'SELECT * FROM users WHERE "id" = ?',
110
205
  GET_USER_BY_NAME: 'SELECT * FROM users WHERE "name" = ?',
111
206
  LIST_USERS: "SELECT * FROM users",
207
+ COUNT_USERS: "SELECT COUNT(*) as count FROM users",
208
+ COUNT_ADMINS: "SELECT COUNT(*) as count FROM users WHERE \"role\" = 'admin'",
112
209
  INSERT_USER: 'INSERT INTO users ("id", "name", "passwordEncrypted", "role", "scopes") VALUES (?, ?, ?, ?, ?)',
113
210
  UPDATE_USER: 'UPDATE users SET "role" = ?, "scopes" = ? WHERE "id" = ?',
114
211
  UPDATE_PASSWORD: 'UPDATE users SET "passwordEncrypted" = ? WHERE "id" = ?',
212
+ BUMP_TOKEN_VERSION: 'UPDATE users SET "tokenVersion" = COALESCE("tokenVersion", 0) + 1 WHERE "id" = ?',
115
213
  DELETE_USER: 'DELETE FROM users WHERE "id" = ?',
116
214
  };