sqlite-hub 0.17.0 → 1.0.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 (83) hide show
  1. package/README.md +101 -19
  2. package/bin/sqlite-hub.js +83 -401
  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 +183 -26
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +79 -19
  30. package/frontend/js/components/queryResults.js +18 -1
  31. package/frontend/js/components/rowEditorPanel.js +42 -34
  32. package/frontend/js/store.js +129 -2
  33. package/frontend/js/utils/jsonPreview.js +31 -0
  34. package/frontend/js/utils/rowEditorValues.js +41 -0
  35. package/frontend/js/utils/tableScrollState.js +39 -0
  36. package/frontend/js/views/charts.js +8 -0
  37. package/frontend/js/views/data.js +4 -1
  38. package/frontend/js/views/editor.js +2 -0
  39. package/frontend/js/views/settings.js +141 -5
  40. package/frontend/styles/components.css +6 -0
  41. package/frontend/styles/tailwind.generated.css +2 -2
  42. package/package.json +6 -6
  43. package/server/middleware/apiTokenAuth.js +30 -0
  44. package/server/middleware/localRequestSecurity.js +84 -0
  45. package/server/routes/connections.js +35 -1
  46. package/server/routes/externalApi.js +222 -0
  47. package/server/routes/settings.js +64 -4
  48. package/server/server.js +34 -2
  49. package/server/services/apiTokenService.js +101 -0
  50. package/server/services/databaseCommandService.js +399 -0
  51. package/server/services/nativeFileDialogService.js +260 -0
  52. package/server/services/sqlite/dataBrowserService.js +0 -4
  53. package/server/services/sqlite/exportService.js +5 -1
  54. package/server/services/sqlite/sqlExecutor.js +51 -2
  55. package/server/services/storage/appStateStore.js +113 -0
  56. package/server/utils/errors.js +7 -0
  57. package/server/utils/sqliteTypes.js +17 -8
  58. package/tests/api-token-auth.test.js +127 -0
  59. package/tests/cli-service-delegation.test.js +43 -0
  60. package/tests/connections-file-dialog-route.test.js +89 -0
  61. package/tests/copy-column-modal.test.js +48 -0
  62. package/tests/database-command-service.test.js +102 -0
  63. package/tests/export-blob.test.js +46 -0
  64. package/tests/form-controls.test.js +34 -0
  65. package/tests/json-preview.test.js +49 -0
  66. package/tests/local-request-security.test.js +85 -0
  67. package/tests/native-file-dialog.test.js +105 -0
  68. package/tests/query-results-truncation.test.js +20 -0
  69. package/tests/row-editor-null-values.test.js +131 -0
  70. package/tests/settings-api-tokens-route.test.js +76 -0
  71. package/tests/settings-view.test.js +47 -0
  72. package/tests/sql-identifier-safety.test.js +9 -3
  73. package/tests/sql-result-limit.test.js +66 -0
  74. package/tests/table-scroll-state.test.js +70 -0
  75. package/frontend/assets/mockups/connections.png +0 -0
  76. package/frontend/assets/mockups/data.png +0 -0
  77. package/frontend/assets/mockups/data_row_editor.png +0 -0
  78. package/frontend/assets/mockups/home.png +0 -0
  79. package/frontend/assets/mockups/sql_editor.png +0 -0
  80. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  81. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  82. package/frontend/assets/mockups/structure.png +0 -0
  83. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -0,0 +1,222 @@
1
+ const express = require("express");
2
+ const { createApiTokenAuth } = require("../middleware/apiTokenAuth");
3
+ const { route, successResponse } = require("../utils/errors");
4
+
5
+ function createExternalApiRouter({ databaseService, tokenService }) {
6
+ const router = express.Router();
7
+
8
+ router.use(
9
+ "/databases/:databaseId",
10
+ createApiTokenAuth({ tokenService })
11
+ );
12
+
13
+ router.get(
14
+ "/databases/:databaseId",
15
+ route((req, res) => {
16
+ res.json(
17
+ successResponse({
18
+ data: databaseService.getDatabase(req.params.databaseId),
19
+ metadata: { databaseId: req.params.databaseId },
20
+ })
21
+ );
22
+ })
23
+ );
24
+
25
+ router.get(
26
+ "/databases/:databaseId/tables",
27
+ route((req, res) => {
28
+ res.json(
29
+ successResponse({
30
+ data: { items: databaseService.listTables(req.params.databaseId) },
31
+ metadata: { databaseId: req.params.databaseId },
32
+ })
33
+ );
34
+ })
35
+ );
36
+
37
+ router.get(
38
+ "/databases/:databaseId/tables/:tableName",
39
+ route((req, res) => {
40
+ res.json(
41
+ successResponse({
42
+ data: databaseService.getTable(req.params.databaseId, req.params.tableName),
43
+ metadata: { databaseId: req.params.databaseId },
44
+ })
45
+ );
46
+ })
47
+ );
48
+
49
+ router.post(
50
+ "/databases/:databaseId/tables/:tableName/row",
51
+ route((req, res) => {
52
+ const result = databaseService.getTableRow(
53
+ req.params.databaseId,
54
+ req.params.tableName,
55
+ req.body?.key
56
+ );
57
+
58
+ res.json(
59
+ successResponse({
60
+ data: result.data,
61
+ metadata: {
62
+ databaseId: req.params.databaseId,
63
+ filename: result.filename,
64
+ identity: result.identity,
65
+ tableName: result.table.name,
66
+ },
67
+ })
68
+ );
69
+ })
70
+ );
71
+
72
+ router.get(
73
+ "/databases/:databaseId/queries",
74
+ route((req, res) => {
75
+ res.json(
76
+ successResponse({
77
+ data: databaseService.listSavedQueries(req.params.databaseId),
78
+ metadata: { databaseId: req.params.databaseId },
79
+ })
80
+ );
81
+ })
82
+ );
83
+
84
+ router.post(
85
+ "/databases/:databaseId/queries/:queryName/execute",
86
+ route((req, res) => {
87
+ const { query, result } = databaseService.executeSavedQuery(
88
+ req.params.databaseId,
89
+ req.params.queryName
90
+ );
91
+
92
+ res.json(
93
+ successResponse({
94
+ message: "Saved query executed successfully.",
95
+ data: result,
96
+ metadata: {
97
+ databaseId: req.params.databaseId,
98
+ query,
99
+ },
100
+ timingMs: result.timingMs,
101
+ })
102
+ );
103
+ })
104
+ );
105
+
106
+ router.get(
107
+ "/databases/:databaseId/queries/:queryName/export",
108
+ route((req, res) => {
109
+ const { query, result } = databaseService.exportSavedQuery(
110
+ req.params.databaseId,
111
+ req.params.queryName,
112
+ req.query.format || "csv"
113
+ );
114
+
115
+ res.json(
116
+ successResponse({
117
+ data: {
118
+ filename: result.filename,
119
+ content: result.content ?? result.csv ?? "",
120
+ format: result.format,
121
+ mimeType: result.mimeType,
122
+ columns: result.columns,
123
+ rowCount: result.rowCount,
124
+ },
125
+ metadata: {
126
+ databaseId: req.params.databaseId,
127
+ query,
128
+ },
129
+ })
130
+ );
131
+ })
132
+ );
133
+
134
+ router.get(
135
+ "/databases/:databaseId/queries/:queryName/notes",
136
+ route((req, res) => {
137
+ const query = databaseService.getSavedQuery(
138
+ req.params.databaseId,
139
+ req.params.queryName
140
+ );
141
+
142
+ res.json(
143
+ successResponse({
144
+ data: { notes: String(query.notes ?? "") },
145
+ metadata: { databaseId: req.params.databaseId, query },
146
+ })
147
+ );
148
+ })
149
+ );
150
+
151
+ router.get(
152
+ "/databases/:databaseId/queries/:queryName",
153
+ route((req, res) => {
154
+ res.json(
155
+ successResponse({
156
+ data: databaseService.getSavedQuery(
157
+ req.params.databaseId,
158
+ req.params.queryName
159
+ ),
160
+ metadata: { databaseId: req.params.databaseId },
161
+ })
162
+ );
163
+ })
164
+ );
165
+
166
+ router.get(
167
+ "/databases/:databaseId/documents",
168
+ route((req, res) => {
169
+ res.json(
170
+ successResponse({
171
+ data: { items: databaseService.listDocuments(req.params.databaseId) },
172
+ metadata: { databaseId: req.params.databaseId },
173
+ })
174
+ );
175
+ })
176
+ );
177
+
178
+ router.get(
179
+ "/databases/:databaseId/documents/:documentName/export",
180
+ route((req, res) => {
181
+ const result = databaseService.exportDocument(
182
+ req.params.databaseId,
183
+ req.params.documentName
184
+ );
185
+
186
+ res.json(
187
+ successResponse({
188
+ data: {
189
+ filename: result.filename,
190
+ content: result.content,
191
+ mimeType: result.mimeType,
192
+ },
193
+ metadata: {
194
+ databaseId: req.params.databaseId,
195
+ document: result.document,
196
+ },
197
+ })
198
+ );
199
+ })
200
+ );
201
+
202
+ router.get(
203
+ "/databases/:databaseId/documents/:documentName",
204
+ route((req, res) => {
205
+ res.json(
206
+ successResponse({
207
+ data: databaseService.getDocument(
208
+ req.params.databaseId,
209
+ req.params.documentName
210
+ ),
211
+ metadata: { databaseId: req.params.databaseId },
212
+ })
213
+ );
214
+ })
215
+ );
216
+
217
+ return router;
218
+ }
219
+
220
+ module.exports = {
221
+ createExternalApiRouter,
222
+ };
@@ -2,7 +2,7 @@ const express = require("express");
2
2
  const Database = require("better-sqlite3");
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
- const { route, successResponse } = require("../utils/errors");
5
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
6
6
 
7
7
  function readAppVersion() {
8
8
  const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
@@ -27,8 +27,35 @@ function readSettingsMetadata() {
27
27
  };
28
28
  }
29
29
 
30
- function createSettingsRouter({ appStateStore }) {
30
+ function getActiveTokenContext({ connectionManager, tokenService }) {
31
+ const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
32
+
33
+ return {
34
+ activeDatabase,
35
+ apiTokens: activeDatabase && tokenService ? tokenService.listTokens(activeDatabase.id) : [],
36
+ };
37
+ }
38
+
39
+ function requireActiveDatabase(connectionManager) {
40
+ const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
41
+
42
+ if (!activeDatabase) {
43
+ throw new DatabaseRequiredError("Select a database before managing API tokens.");
44
+ }
45
+
46
+ return activeDatabase;
47
+ }
48
+
49
+ function buildSettingsMetadata(context) {
50
+ return {
51
+ ...readSettingsMetadata(),
52
+ ...getActiveTokenContext(context),
53
+ };
54
+ }
55
+
56
+ function createSettingsRouter({ appStateStore, connectionManager, tokenService }) {
31
57
  const router = express.Router();
58
+ const context = { connectionManager, tokenService };
32
59
 
33
60
  router.get(
34
61
  "/",
@@ -36,7 +63,7 @@ function createSettingsRouter({ appStateStore }) {
36
63
  res.json(
37
64
  successResponse({
38
65
  data: appStateStore.getSettings(),
39
- metadata: readSettingsMetadata(),
66
+ metadata: buildSettingsMetadata(context),
40
67
  })
41
68
  );
42
69
  })
@@ -50,7 +77,39 @@ function createSettingsRouter({ appStateStore }) {
50
77
  successResponse({
51
78
  message: "Settings updated.",
52
79
  data: settings,
53
- metadata: readSettingsMetadata(),
80
+ metadata: buildSettingsMetadata(context),
81
+ })
82
+ );
83
+ })
84
+ );
85
+
86
+ router.post(
87
+ "/api-tokens",
88
+ route((req, res) => {
89
+ const activeDatabase = requireActiveDatabase(connectionManager);
90
+ const token = tokenService.createToken(activeDatabase.id, req.body?.name);
91
+
92
+ res.status(201).json(
93
+ successResponse({
94
+ message: "API token created. It will only be shown once.",
95
+ data: token,
96
+ metadata: buildSettingsMetadata(context),
97
+ })
98
+ );
99
+ })
100
+ );
101
+
102
+ router.delete(
103
+ "/api-tokens/:tokenId",
104
+ route((req, res) => {
105
+ const activeDatabase = requireActiveDatabase(connectionManager);
106
+ const result = tokenService.deleteToken(activeDatabase.id, req.params.tokenId);
107
+
108
+ res.json(
109
+ successResponse({
110
+ message: "API token deleted.",
111
+ data: result,
112
+ metadata: buildSettingsMetadata(context),
54
113
  })
55
114
  );
56
115
  })
@@ -61,6 +120,7 @@ function createSettingsRouter({ appStateStore }) {
61
120
 
62
121
  module.exports = {
63
122
  createSettingsRouter,
123
+ buildSettingsMetadata,
64
124
  readSettingsMetadata,
65
125
  readSqliteVersion,
66
126
  };
package/server/server.js CHANGED
@@ -3,6 +3,11 @@ const helmet = require("helmet");
3
3
  const rateLimit = require("express-rate-limit");
4
4
  const path = require("node:path");
5
5
  const { errorMiddleware } = require("./utils/errors");
6
+ const {
7
+ LOOPBACK_HOST,
8
+ listenOnLoopback,
9
+ localRequestSecurity,
10
+ } = require("./middleware/localRequestSecurity");
6
11
  const { resolveAppStatePaths } = require("./utils/appPaths");
7
12
  const { AppStateStore } = require("./services/storage/appStateStore");
8
13
  const { ConnectionManager } = require("./services/sqlite/connectionManager");
@@ -10,11 +15,14 @@ const { OverviewService } = require("./services/sqlite/overviewService");
10
15
  const { SqlExecutor } = require("./services/sqlite/sqlExecutor");
11
16
  const { ImportService } = require("./services/sqlite/importService");
12
17
  const { BackupService } = require("./services/sqlite/backupService");
18
+ const { NativeFileDialogService } = require("./services/nativeFileDialogService");
13
19
  const { ExportService } = require("./services/sqlite/exportService");
14
20
  const { StructureService } = require("./services/sqlite/structureService");
15
21
  const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
16
22
  const { TableDesignerService } = require("./services/sqlite/tableDesignerService");
17
23
  const { MediaTaggingService } = require("./services/sqlite/mediaTaggingService");
24
+ const { ApiTokenService } = require("./services/apiTokenService");
25
+ const { DatabaseCommandService } = require("./services/databaseCommandService");
18
26
  const { createConnectionsRouter } = require("./routes/connections");
19
27
  const { createOverviewRouter } = require("./routes/overview");
20
28
  const { createSqlRouter } = require("./routes/sql");
@@ -26,6 +34,7 @@ const { createMediaTaggingRouter } = require("./routes/mediaTagging");
26
34
  const { createSettingsRouter } = require("./routes/settings");
27
35
  const { createExportRouter } = require("./routes/export");
28
36
  const { createDocumentsRouter } = require("./routes/documents");
37
+ const { createExternalApiRouter } = require("./routes/externalApi");
29
38
 
30
39
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
31
40
  const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
@@ -37,6 +46,7 @@ const {
37
46
  legacyDatabasePaths: LEGACY_DATABASE_PATHS,
38
47
  } = resolveAppStatePaths(PACKAGE_ROOT);
39
48
  const DEFAULT_PORT = 4173;
49
+ const DEFAULT_HOST = LOOPBACK_HOST;
40
50
 
41
51
  const appStateStore = new AppStateStore(APP_STATE_DB_PATH, {
42
52
  legacyFilePath: LEGACY_STATE_PATH,
@@ -47,6 +57,7 @@ const overviewService = new OverviewService({ connectionManager });
47
57
  const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
48
58
  const importService = new ImportService({ connectionManager });
49
59
  const backupService = new BackupService({ connectionManager });
60
+ const nativeFileDialogService = new NativeFileDialogService();
50
61
  const exportService = new ExportService({
51
62
  appStateStore,
52
63
  connectionManager,
@@ -56,6 +67,8 @@ const structureService = new StructureService({ connectionManager, appStateStore
56
67
  const dataBrowserService = new DataBrowserService({ connectionManager });
57
68
  const tableDesignerService = new TableDesignerService({ connectionManager });
58
69
  const mediaTaggingService = new MediaTaggingService({ connectionManager, appStateStore });
70
+ const apiTokenService = new ApiTokenService({ appStateStore });
71
+ const databaseCommandService = new DatabaseCommandService({ appStateStore });
59
72
 
60
73
  connectionManager.initialize();
61
74
 
@@ -66,6 +79,7 @@ app.use(
66
79
  contentSecurityPolicy: false,
67
80
  })
68
81
  );
82
+ app.use("/api", localRequestSecurity);
69
83
  app.use(
70
84
  "/api",
71
85
  rateLimit({
@@ -97,6 +111,7 @@ app.use(
97
111
  connectionManager,
98
112
  importService,
99
113
  backupService,
114
+ nativeFileDialogService,
100
115
  })
101
116
  );
102
117
  app.use("/api/db", createOverviewRouter({ overviewService }));
@@ -106,9 +121,23 @@ app.use("/api/structure", createStructureRouter({ structureService }));
106
121
  app.use("/api/data", createDataRouter({ dataBrowserService }));
107
122
  app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
108
123
  app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
109
- app.use("/api/settings", createSettingsRouter({ appStateStore }));
124
+ app.use(
125
+ "/api/settings",
126
+ createSettingsRouter({
127
+ appStateStore,
128
+ connectionManager,
129
+ tokenService: apiTokenService,
130
+ })
131
+ );
110
132
  app.use("/api/export", createExportRouter({ exportService }));
111
133
  app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
134
+ app.use(
135
+ "/api/v1",
136
+ createExternalApiRouter({
137
+ databaseService: databaseCommandService,
138
+ tokenService: apiTokenService,
139
+ })
140
+ );
112
141
 
113
142
  // auth: public favicon response; it exposes no application data.
114
143
  app.get("/favicon.ico", (req, res) => {
@@ -191,7 +220,7 @@ function startServer({ port } = {}) {
191
220
  const resolvedPort = resolvePort(port);
192
221
 
193
222
  return new Promise((resolve, reject) => {
194
- const server = app.listen(resolvedPort);
223
+ const server = listenOnLoopback(app, resolvedPort);
195
224
 
196
225
  server.once("error", reject);
197
226
  server.once("listening", () => {
@@ -217,7 +246,10 @@ if (require.main === module) {
217
246
  module.exports = {
218
247
  app,
219
248
  appStateStore,
249
+ apiTokenService,
220
250
  connectionManager,
251
+ databaseCommandService,
252
+ DEFAULT_HOST,
221
253
  DEFAULT_PORT,
222
254
  parsePortArgument,
223
255
  resolvePort,
@@ -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
+ };