sqlite-hub 0.17.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +143 -47
  2. package/bin/sqlite-hub.js +206 -444
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +263 -34
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +77 -21
  30. package/frontend/js/components/queryChartRenderer.js +28 -3
  31. package/frontend/js/components/queryEditor.js +20 -24
  32. package/frontend/js/components/queryHistoryDetail.js +1 -1
  33. package/frontend/js/components/queryHistoryHeader.js +48 -0
  34. package/frontend/js/components/queryHistoryList.js +132 -0
  35. package/frontend/js/components/queryHistoryPanel.js +72 -136
  36. package/frontend/js/components/structureGraph.js +699 -89
  37. package/frontend/js/components/tableDesignerEditor.js +3 -5
  38. package/frontend/js/store.js +188 -7
  39. package/frontend/js/utils/exportFilenames.js +3 -1
  40. package/frontend/js/views/charts.js +320 -169
  41. package/frontend/js/views/connections.js +59 -64
  42. package/frontend/js/views/data.js +12 -20
  43. package/frontend/js/views/editor.js +0 -2
  44. package/frontend/js/views/settings.js +219 -5
  45. package/frontend/js/views/structure.js +27 -13
  46. package/frontend/styles/components.css +155 -0
  47. package/frontend/styles/structure-graph.css +140 -35
  48. package/frontend/styles/tailwind.generated.css +2 -2
  49. package/frontend/styles/views.css +12 -6
  50. package/package.json +7 -6
  51. package/server/middleware/apiTokenAuth.js +30 -0
  52. package/server/routes/connections.js +17 -0
  53. package/server/routes/export.js +89 -22
  54. package/server/routes/externalApi.js +304 -0
  55. package/server/routes/settings.js +90 -21
  56. package/server/server.js +22 -1
  57. package/server/services/apiTokenService.js +101 -0
  58. package/server/services/appInfoService.js +215 -0
  59. package/server/services/databaseCommandService.js +443 -0
  60. package/server/services/nativeFileDialogService.js +93 -1
  61. package/server/services/sqlite/exportService.js +307 -22
  62. package/server/services/storage/appStateStore.js +113 -0
  63. package/server/utils/errors.js +7 -0
  64. package/tests/api-token-auth.test.js +236 -0
  65. package/tests/cli-args.test.js +16 -3
  66. package/tests/cli-service-delegation.test.js +43 -0
  67. package/tests/connections-file-dialog-route.test.js +43 -0
  68. package/tests/copy-column-modal.test.js +34 -0
  69. package/tests/database-command-service.test.js +139 -0
  70. package/tests/export-blob.test.js +54 -1
  71. package/tests/export-filenames.test.js +4 -0
  72. package/tests/form-controls.test.js +34 -0
  73. package/tests/native-file-dialog.test.js +27 -0
  74. package/tests/settings-api-tokens-route.test.js +97 -0
  75. package/tests/settings-metadata.test.js +99 -1
  76. package/tests/settings-view.test.js +75 -0
  77. package/frontend/assets/mockups/connections.png +0 -0
  78. package/frontend/assets/mockups/data.png +0 -0
  79. package/frontend/assets/mockups/data_row_editor.png +0 -0
  80. package/frontend/assets/mockups/home.png +0 -0
  81. package/frontend/assets/mockups/sql_editor.png +0 -0
  82. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  83. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  84. package/frontend/assets/mockups/structure.png +0 -0
  85. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -0,0 +1,304 @@
1
+ const express = require("express");
2
+ const { createApiTokenAuth } = require("../middleware/apiTokenAuth");
3
+ const { readBearerToken } = require("../middleware/apiTokenAuth");
4
+ const { AuthenticationError, ValidationError, route, successResponse } = require("../utils/errors");
5
+ const { buildAppInfo } = require("../services/appInfoService");
6
+
7
+ function buildRequestBaseUrl(req) {
8
+ const host = req.get("host") ?? `127.0.0.1:${req.socket.localPort ?? ""}`;
9
+ return `${req.protocol}://${host}`;
10
+ }
11
+
12
+ function readDatabaseId(req) {
13
+ return String(req.body?.databaseId ?? req.query.databaseId ?? "").trim();
14
+ }
15
+
16
+ function readSqlText(req) {
17
+ return String(req.body?.sql ?? req.body?.query ?? req.query.sql ?? req.query.query ?? "");
18
+ }
19
+
20
+ function readStoreName(req) {
21
+ return String(
22
+ req.body?.store ??
23
+ req.body?.storeName ??
24
+ req.body?.name ??
25
+ req.query.store ??
26
+ req.query.storeName ??
27
+ req.query.name ??
28
+ ""
29
+ ).trim();
30
+ }
31
+
32
+ function authenticateDatabaseRequest(req, tokenService, databaseId) {
33
+ const token = readBearerToken(req.get("authorization"));
34
+
35
+ if (!token) {
36
+ throw new AuthenticationError("Bearer API token is required.", {
37
+ code: "API_TOKEN_REQUIRED",
38
+ });
39
+ }
40
+
41
+ return tokenService.authenticate(databaseId, token);
42
+ }
43
+
44
+ function createExternalApiRouter({ databaseService, tokenService, appInfoService = buildAppInfo }) {
45
+ const router = express.Router();
46
+
47
+ router.get(
48
+ "/info",
49
+ route(async (req, res) => {
50
+ const port = Number(req.socket.localPort);
51
+ const data = await appInfoService({
52
+ port: Number.isInteger(port) ? port : null,
53
+ url: buildRequestBaseUrl(req),
54
+ });
55
+
56
+ res.json(successResponse({ data }));
57
+ })
58
+ );
59
+
60
+ router.post(
61
+ "/query",
62
+ route((req, res) => {
63
+ const databaseId = readDatabaseId(req);
64
+ const sql = readSqlText(req);
65
+ const storeName = readStoreName(req);
66
+
67
+ if (!databaseId) {
68
+ throw new ValidationError("databaseId is required.");
69
+ }
70
+
71
+ authenticateDatabaseRequest(req, tokenService, databaseId);
72
+
73
+ const { result } = databaseService.executeRawQuery(databaseId, sql, { storeName });
74
+
75
+ res.json(
76
+ successResponse({
77
+ message: "SQL executed successfully.",
78
+ data: result,
79
+ metadata: {
80
+ databaseId,
81
+ stored: Boolean(result.storedQuery),
82
+ },
83
+ timingMs: result.timingMs,
84
+ readOnly: false,
85
+ })
86
+ );
87
+ })
88
+ );
89
+
90
+ router.use(
91
+ "/databases/:databaseId",
92
+ createApiTokenAuth({ tokenService })
93
+ );
94
+
95
+ router.get(
96
+ "/databases/:databaseId",
97
+ route((req, res) => {
98
+ res.json(
99
+ successResponse({
100
+ data: databaseService.getDatabase(req.params.databaseId),
101
+ metadata: { databaseId: req.params.databaseId },
102
+ })
103
+ );
104
+ })
105
+ );
106
+
107
+ router.get(
108
+ "/databases/:databaseId/tables",
109
+ route((req, res) => {
110
+ res.json(
111
+ successResponse({
112
+ data: { items: databaseService.listTables(req.params.databaseId) },
113
+ metadata: { databaseId: req.params.databaseId },
114
+ })
115
+ );
116
+ })
117
+ );
118
+
119
+ router.get(
120
+ "/databases/:databaseId/tables/:tableName",
121
+ route((req, res) => {
122
+ res.json(
123
+ successResponse({
124
+ data: databaseService.getTable(req.params.databaseId, req.params.tableName),
125
+ metadata: { databaseId: req.params.databaseId },
126
+ })
127
+ );
128
+ })
129
+ );
130
+
131
+ router.post(
132
+ "/databases/:databaseId/tables/:tableName/row",
133
+ route((req, res) => {
134
+ const result = databaseService.getTableRow(
135
+ req.params.databaseId,
136
+ req.params.tableName,
137
+ req.body?.key
138
+ );
139
+
140
+ res.json(
141
+ successResponse({
142
+ data: result.data,
143
+ metadata: {
144
+ databaseId: req.params.databaseId,
145
+ filename: result.filename,
146
+ identity: result.identity,
147
+ tableName: result.table.name,
148
+ },
149
+ })
150
+ );
151
+ })
152
+ );
153
+
154
+ router.get(
155
+ "/databases/:databaseId/queries",
156
+ route((req, res) => {
157
+ res.json(
158
+ successResponse({
159
+ data: databaseService.listSavedQueries(req.params.databaseId),
160
+ metadata: { databaseId: req.params.databaseId },
161
+ })
162
+ );
163
+ })
164
+ );
165
+
166
+ router.post(
167
+ "/databases/:databaseId/queries/:queryName/execute",
168
+ route((req, res) => {
169
+ const { query, result } = databaseService.executeSavedQuery(
170
+ req.params.databaseId,
171
+ req.params.queryName
172
+ );
173
+
174
+ res.json(
175
+ successResponse({
176
+ message: "Saved query executed successfully.",
177
+ data: result,
178
+ metadata: {
179
+ databaseId: req.params.databaseId,
180
+ query,
181
+ },
182
+ timingMs: result.timingMs,
183
+ })
184
+ );
185
+ })
186
+ );
187
+
188
+ router.get(
189
+ "/databases/:databaseId/queries/:queryName/export",
190
+ route((req, res) => {
191
+ const { query, result } = databaseService.exportSavedQuery(
192
+ req.params.databaseId,
193
+ req.params.queryName,
194
+ req.query.format || "csv"
195
+ );
196
+
197
+ res.json(
198
+ successResponse({
199
+ data: {
200
+ filename: result.filename,
201
+ content: result.content ?? result.csv ?? "",
202
+ format: result.format,
203
+ mimeType: result.mimeType,
204
+ columns: result.columns,
205
+ rowCount: result.rowCount,
206
+ },
207
+ metadata: {
208
+ databaseId: req.params.databaseId,
209
+ query,
210
+ },
211
+ })
212
+ );
213
+ })
214
+ );
215
+
216
+ router.get(
217
+ "/databases/:databaseId/queries/:queryName/notes",
218
+ route((req, res) => {
219
+ const query = databaseService.getSavedQuery(
220
+ req.params.databaseId,
221
+ req.params.queryName
222
+ );
223
+
224
+ res.json(
225
+ successResponse({
226
+ data: { notes: String(query.notes ?? "") },
227
+ metadata: { databaseId: req.params.databaseId, query },
228
+ })
229
+ );
230
+ })
231
+ );
232
+
233
+ router.get(
234
+ "/databases/:databaseId/queries/:queryName",
235
+ route((req, res) => {
236
+ res.json(
237
+ successResponse({
238
+ data: databaseService.getSavedQuery(
239
+ req.params.databaseId,
240
+ req.params.queryName
241
+ ),
242
+ metadata: { databaseId: req.params.databaseId },
243
+ })
244
+ );
245
+ })
246
+ );
247
+
248
+ router.get(
249
+ "/databases/:databaseId/documents",
250
+ route((req, res) => {
251
+ res.json(
252
+ successResponse({
253
+ data: { items: databaseService.listDocuments(req.params.databaseId) },
254
+ metadata: { databaseId: req.params.databaseId },
255
+ })
256
+ );
257
+ })
258
+ );
259
+
260
+ router.get(
261
+ "/databases/:databaseId/documents/:documentName/export",
262
+ route((req, res) => {
263
+ const result = databaseService.exportDocument(
264
+ req.params.databaseId,
265
+ req.params.documentName
266
+ );
267
+
268
+ res.json(
269
+ successResponse({
270
+ data: {
271
+ filename: result.filename,
272
+ content: result.content,
273
+ mimeType: result.mimeType,
274
+ },
275
+ metadata: {
276
+ databaseId: req.params.databaseId,
277
+ document: result.document,
278
+ },
279
+ })
280
+ );
281
+ })
282
+ );
283
+
284
+ router.get(
285
+ "/databases/:databaseId/documents/:documentName",
286
+ route((req, res) => {
287
+ res.json(
288
+ successResponse({
289
+ data: databaseService.getDocument(
290
+ req.params.databaseId,
291
+ req.params.documentName
292
+ ),
293
+ metadata: { databaseId: req.params.databaseId },
294
+ })
295
+ );
296
+ })
297
+ );
298
+
299
+ return router;
300
+ }
301
+
302
+ module.exports = {
303
+ createExternalApiRouter,
304
+ };
@@ -1,34 +1,43 @@
1
1
  const express = require("express");
2
- const Database = require("better-sqlite3");
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const { route, successResponse } = require("../utils/errors");
6
-
7
- function readAppVersion() {
8
- const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
9
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
10
- return packageJson.version ?? "0.0.0";
2
+ const { AppError, DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
+ const {
4
+ checkLatestAppVersion,
5
+ compareSemver,
6
+ isNewerVersion,
7
+ readSettingsMetadata,
8
+ readSqliteVersion,
9
+ } = require("../services/appInfoService");
10
+
11
+ function getActiveTokenContext({ connectionManager, tokenService }) {
12
+ const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
13
+
14
+ return {
15
+ activeDatabase,
16
+ apiTokens: activeDatabase && tokenService ? tokenService.listTokens(activeDatabase.id) : [],
17
+ };
11
18
  }
12
19
 
13
- function readSqliteVersion() {
14
- const db = new Database(":memory:");
20
+ function requireActiveDatabase(connectionManager) {
21
+ const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
15
22
 
16
- try {
17
- return db.prepare("SELECT sqlite_version() AS version").get().version ?? "unknown";
18
- } finally {
19
- db.close();
23
+ if (!activeDatabase) {
24
+ throw new DatabaseRequiredError("Select a database before managing API tokens.");
20
25
  }
26
+
27
+ return activeDatabase;
21
28
  }
22
29
 
23
- function readSettingsMetadata() {
30
+ function buildSettingsMetadata(context) {
24
31
  return {
25
- appVersion: readAppVersion(),
26
- sqliteVersion: readSqliteVersion(),
32
+ ...readSettingsMetadata(),
33
+ ...getActiveTokenContext(context),
27
34
  };
28
35
  }
29
36
 
30
- function createSettingsRouter({ appStateStore }) {
37
+ function createSettingsRouter({ appStateStore, connectionManager, tokenService, versionCheckService }) {
31
38
  const router = express.Router();
39
+ const context = { connectionManager, tokenService };
40
+ const checkVersion = versionCheckService ?? checkLatestAppVersion;
32
41
 
33
42
  router.get(
34
43
  "/",
@@ -36,7 +45,7 @@ function createSettingsRouter({ appStateStore }) {
36
45
  res.json(
37
46
  successResponse({
38
47
  data: appStateStore.getSettings(),
39
- metadata: readSettingsMetadata(),
48
+ metadata: buildSettingsMetadata(context),
40
49
  })
41
50
  );
42
51
  })
@@ -50,7 +59,63 @@ function createSettingsRouter({ appStateStore }) {
50
59
  successResponse({
51
60
  message: "Settings updated.",
52
61
  data: settings,
53
- metadata: readSettingsMetadata(),
62
+ metadata: buildSettingsMetadata(context),
63
+ })
64
+ );
65
+ })
66
+ );
67
+
68
+ router.get(
69
+ "/version-check",
70
+ route(async (req, res) => {
71
+ try {
72
+ const result = await checkVersion();
73
+
74
+ res.json(
75
+ successResponse({
76
+ data: result,
77
+ metadata: readSettingsMetadata(),
78
+ })
79
+ );
80
+ } catch (error) {
81
+ throw new AppError("Version check failed. Check your internet connection and try again.", 502, {
82
+ code: "VERSION_CHECK_FAILED",
83
+ details: {
84
+ source: "npm",
85
+ message: error.message,
86
+ },
87
+ });
88
+ }
89
+ })
90
+ );
91
+
92
+ router.post(
93
+ "/api-tokens",
94
+ route((req, res) => {
95
+ const activeDatabase = requireActiveDatabase(connectionManager);
96
+ const token = tokenService.createToken(activeDatabase.id, req.body?.name);
97
+
98
+ res.status(201).json(
99
+ successResponse({
100
+ message: "API token created. It will only be shown once.",
101
+ data: token,
102
+ metadata: buildSettingsMetadata(context),
103
+ })
104
+ );
105
+ })
106
+ );
107
+
108
+ router.delete(
109
+ "/api-tokens/:tokenId",
110
+ route((req, res) => {
111
+ const activeDatabase = requireActiveDatabase(connectionManager);
112
+ const result = tokenService.deleteToken(activeDatabase.id, req.params.tokenId);
113
+
114
+ res.json(
115
+ successResponse({
116
+ message: "API token deleted.",
117
+ data: result,
118
+ metadata: buildSettingsMetadata(context),
54
119
  })
55
120
  );
56
121
  })
@@ -61,6 +126,10 @@ function createSettingsRouter({ appStateStore }) {
61
126
 
62
127
  module.exports = {
63
128
  createSettingsRouter,
129
+ buildSettingsMetadata,
130
+ checkLatestAppVersion,
131
+ compareSemver,
132
+ isNewerVersion,
64
133
  readSettingsMetadata,
65
134
  readSqliteVersion,
66
135
  };
package/server/server.js CHANGED
@@ -21,6 +21,8 @@ const { StructureService } = require("./services/sqlite/structureService");
21
21
  const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
22
22
  const { TableDesignerService } = require("./services/sqlite/tableDesignerService");
23
23
  const { MediaTaggingService } = require("./services/sqlite/mediaTaggingService");
24
+ const { ApiTokenService } = require("./services/apiTokenService");
25
+ const { DatabaseCommandService } = require("./services/databaseCommandService");
24
26
  const { createConnectionsRouter } = require("./routes/connections");
25
27
  const { createOverviewRouter } = require("./routes/overview");
26
28
  const { createSqlRouter } = require("./routes/sql");
@@ -32,6 +34,7 @@ const { createMediaTaggingRouter } = require("./routes/mediaTagging");
32
34
  const { createSettingsRouter } = require("./routes/settings");
33
35
  const { createExportRouter } = require("./routes/export");
34
36
  const { createDocumentsRouter } = require("./routes/documents");
37
+ const { createExternalApiRouter } = require("./routes/externalApi");
35
38
 
36
39
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
37
40
  const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
@@ -64,6 +67,8 @@ const structureService = new StructureService({ connectionManager, appStateStore
64
67
  const dataBrowserService = new DataBrowserService({ connectionManager });
65
68
  const tableDesignerService = new TableDesignerService({ connectionManager });
66
69
  const mediaTaggingService = new MediaTaggingService({ connectionManager, appStateStore });
70
+ const apiTokenService = new ApiTokenService({ appStateStore });
71
+ const databaseCommandService = new DatabaseCommandService({ appStateStore });
67
72
 
68
73
  connectionManager.initialize();
69
74
 
@@ -116,9 +121,23 @@ app.use("/api/structure", createStructureRouter({ structureService }));
116
121
  app.use("/api/data", createDataRouter({ dataBrowserService }));
117
122
  app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
118
123
  app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
119
- app.use("/api/settings", createSettingsRouter({ appStateStore }));
124
+ app.use(
125
+ "/api/settings",
126
+ createSettingsRouter({
127
+ appStateStore,
128
+ connectionManager,
129
+ tokenService: apiTokenService,
130
+ })
131
+ );
120
132
  app.use("/api/export", createExportRouter({ exportService }));
121
133
  app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
134
+ app.use(
135
+ "/api/v1",
136
+ createExternalApiRouter({
137
+ databaseService: databaseCommandService,
138
+ tokenService: apiTokenService,
139
+ })
140
+ );
122
141
 
123
142
  // auth: public favicon response; it exposes no application data.
124
143
  app.get("/favicon.ico", (req, res) => {
@@ -227,7 +246,9 @@ if (require.main === module) {
227
246
  module.exports = {
228
247
  app,
229
248
  appStateStore,
249
+ apiTokenService,
230
250
  connectionManager,
251
+ databaseCommandService,
231
252
  DEFAULT_HOST,
232
253
  DEFAULT_PORT,
233
254
  parsePortArgument,
@@ -0,0 +1,101 @@
1
+ const crypto = require("node:crypto");
2
+ const { AuthenticationError, NotFoundError, ValidationError } = require("../utils/errors");
3
+
4
+ const TOKEN_PREFIX = "shub_";
5
+
6
+ function hashApiToken(token) {
7
+ return crypto.createHash("sha256").update(String(token ?? ""), "utf8").digest("hex");
8
+ }
9
+
10
+ function normalizeDatabaseKey(databaseKey) {
11
+ const normalized = String(databaseKey ?? "").trim();
12
+
13
+ if (!normalized) {
14
+ throw new ValidationError("Database key is required.");
15
+ }
16
+
17
+ return normalized;
18
+ }
19
+
20
+ function normalizeTokenName(name) {
21
+ const normalized = String(name ?? "").trim() || "API token";
22
+
23
+ if (normalized.length > 80) {
24
+ throw new ValidationError("Token name must not exceed 80 characters.");
25
+ }
26
+
27
+ return normalized;
28
+ }
29
+
30
+ class ApiTokenService {
31
+ constructor({ appStateStore }) {
32
+ this.appStateStore = appStateStore;
33
+ }
34
+
35
+ assertDatabaseExists(databaseKey) {
36
+ const normalizedDatabaseKey = normalizeDatabaseKey(databaseKey);
37
+ const connection = this.appStateStore
38
+ .getRecentConnections()
39
+ .find((candidate) => candidate.id === normalizedDatabaseKey);
40
+
41
+ if (!connection) {
42
+ throw new NotFoundError(`Database not found: ${normalizedDatabaseKey}`);
43
+ }
44
+
45
+ return connection;
46
+ }
47
+
48
+ listTokens(databaseKey) {
49
+ const connection = this.assertDatabaseExists(databaseKey);
50
+ return this.appStateStore.listApiTokens(connection.id);
51
+ }
52
+
53
+ createToken(databaseKey, name) {
54
+ const connection = this.assertDatabaseExists(databaseKey);
55
+ const token = `${TOKEN_PREFIX}${crypto.randomBytes(32).toString("base64url")}`;
56
+ const record = this.appStateStore.createApiToken({
57
+ databaseKey: connection.id,
58
+ name: normalizeTokenName(name),
59
+ tokenHash: hashApiToken(token),
60
+ tokenPrefix: token.slice(0, 13),
61
+ });
62
+
63
+ return {
64
+ ...record,
65
+ token,
66
+ };
67
+ }
68
+
69
+ deleteToken(databaseKey, tokenId) {
70
+ const connection = this.assertDatabaseExists(databaseKey);
71
+ return this.appStateStore.deleteApiToken(connection.id, tokenId);
72
+ }
73
+
74
+ authenticate(databaseKey, token) {
75
+ const normalizedDatabaseKey = normalizeDatabaseKey(databaseKey);
76
+ const normalizedToken = String(token ?? "").trim();
77
+
78
+ if (!normalizedToken) {
79
+ throw new AuthenticationError("API token is required.", {
80
+ code: "API_TOKEN_REQUIRED",
81
+ });
82
+ }
83
+
84
+ const record = this.appStateStore.findApiTokenByHash(hashApiToken(normalizedToken));
85
+
86
+ if (!record || record.databaseKey !== normalizedDatabaseKey) {
87
+ throw new AuthenticationError("API token is invalid for this database.", {
88
+ code: "API_TOKEN_INVALID",
89
+ });
90
+ }
91
+
92
+ this.appStateStore.touchApiToken(record.id);
93
+ return record;
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ ApiTokenService,
99
+ TOKEN_PREFIX,
100
+ hashApiToken,
101
+ };