@activeledger/activestorage 4.5.13 → 4.5.14

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 (125) hide show
  1. package/es/cache.d.ts +18 -0
  2. package/es/cache.d.ts.map +1 -0
  3. package/es/cache.js +57 -0
  4. package/es/cache.js.map +1 -0
  5. package/es/datastore.d.ts +11 -0
  6. package/es/datastore.d.ts.map +1 -0
  7. package/es/datastore.js +67 -0
  8. package/es/datastore.js.map +1 -0
  9. package/es/driver.d.ts +15 -0
  10. package/es/driver.d.ts.map +1 -0
  11. package/es/driver.js +2 -0
  12. package/es/driver.js.map +1 -0
  13. package/es/drivers/leveldb.d.ts +20 -0
  14. package/es/drivers/leveldb.d.ts.map +1 -0
  15. package/es/drivers/leveldb.js +121 -0
  16. package/es/drivers/leveldb.js.map +1 -0
  17. package/es/drivers/rocksdb.d.ts +20 -0
  18. package/es/drivers/rocksdb.d.ts.map +1 -0
  19. package/es/drivers/rocksdb.js +98 -0
  20. package/es/drivers/rocksdb.js.map +1 -0
  21. package/es/heartbeat.d.ts +6 -0
  22. package/es/heartbeat.d.ts.map +1 -0
  23. package/es/heartbeat.js +15 -0
  24. package/es/heartbeat.js.map +1 -0
  25. package/es/index.d.ts +3 -0
  26. package/es/index.d.ts.map +1 -0
  27. package/es/index.js +3 -0
  28. package/es/index.js.map +1 -0
  29. package/es/levelme.d.ts +82 -0
  30. package/es/levelme.d.ts.map +1 -0
  31. package/es/levelme.js +488 -0
  32. package/es/levelme.js.map +1 -0
  33. package/es/newlinestream.d.ts +3 -0
  34. package/es/newlinestream.d.ts.map +1 -0
  35. package/es/newlinestream.js +28 -0
  36. package/es/newlinestream.js.map +1 -0
  37. package/es/selfhost.d.ts +2 -0
  38. package/es/selfhost.d.ts.map +1 -0
  39. package/es/selfhost.js +479 -0
  40. package/es/selfhost.js.map +1 -0
  41. package/es/sse.d.ts +10 -0
  42. package/es/sse.d.ts.map +1 -0
  43. package/es/sse.js +41 -0
  44. package/es/sse.js.map +1 -0
  45. package/lib/cache.d.ts +18 -0
  46. package/lib/cache.d.ts.map +1 -0
  47. package/lib/cache.js +62 -0
  48. package/lib/cache.js.map +1 -0
  49. package/lib/datastore.d.ts +11 -0
  50. package/lib/datastore.d.ts.map +1 -0
  51. package/lib/datastore.js +94 -0
  52. package/lib/datastore.js.map +1 -0
  53. package/lib/driver.d.ts +15 -0
  54. package/lib/driver.d.ts.map +1 -0
  55. package/lib/driver.js +3 -0
  56. package/lib/driver.js.map +1 -0
  57. package/lib/drivers/leveldb.d.ts +20 -0
  58. package/lib/drivers/leveldb.d.ts.map +1 -0
  59. package/lib/drivers/leveldb.js +125 -0
  60. package/lib/drivers/leveldb.js.map +1 -0
  61. package/lib/drivers/rocksdb.d.ts +20 -0
  62. package/lib/drivers/rocksdb.d.ts.map +1 -0
  63. package/lib/drivers/rocksdb.js +102 -0
  64. package/lib/drivers/rocksdb.js.map +1 -0
  65. package/lib/heartbeat.d.ts +6 -0
  66. package/lib/heartbeat.d.ts.map +1 -0
  67. package/lib/heartbeat.js +19 -0
  68. package/lib/heartbeat.js.map +1 -0
  69. package/lib/index.d.ts +3 -0
  70. package/lib/index.d.ts.map +1 -0
  71. package/lib/index.js +6 -0
  72. package/lib/index.js.map +1 -0
  73. package/lib/levelme.d.ts +82 -0
  74. package/lib/levelme.d.ts.map +1 -0
  75. package/lib/levelme.js +515 -0
  76. package/lib/levelme.js.map +1 -0
  77. package/lib/newlinestream.d.ts +3 -0
  78. package/lib/newlinestream.d.ts.map +1 -0
  79. package/lib/newlinestream.js +31 -0
  80. package/lib/newlinestream.js.map +1 -0
  81. package/lib/selfhost.d.ts +2 -0
  82. package/lib/selfhost.d.ts.map +1 -0
  83. package/lib/selfhost.js +504 -0
  84. package/lib/selfhost.js.map +1 -0
  85. package/lib/sse.d.ts +10 -0
  86. package/lib/sse.d.ts.map +1 -0
  87. package/lib/sse.js +45 -0
  88. package/lib/sse.js.map +1 -0
  89. package/package.json +9 -5
  90. package/src/cache.ts +0 -72
  91. package/src/datastore.ts +0 -191
  92. package/src/driver.ts +0 -18
  93. package/src/drivers/leveldb.ts +0 -145
  94. package/src/drivers/rocksdb.ts +0 -118
  95. package/src/heartbeat.ts +0 -60
  96. package/src/index.ts +0 -25
  97. package/src/levelme.ts +0 -938
  98. package/src/newlinestream.ts +0 -32
  99. package/src/selfhost.ts +0 -1104
  100. package/src/sse.ts +0 -136
  101. package/src/types/rocksdb.d.ts +0 -1
  102. package/tsconfig.json +0 -8
  103. /package/{src → lib}/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +0 -0
  104. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
  105. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
  106. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
  107. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
  108. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
  109. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
  110. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
  111. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger.png +0 -0
  112. /package/{src → lib}/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
  113. /package/{src → lib}/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
  114. /package/{src → lib}/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
  115. /package/{src → lib}/fauxton/dashboard.assets/img/fauxtonicon5.svg +0 -0
  116. /package/{src → lib}/fauxton/dashboard.assets/img/fontawesome-webfont.svg +0 -0
  117. /package/{src → lib}/fauxton/dashboard.assets/img/github.png +0 -0
  118. /package/{src → lib}/fauxton/dashboard.assets/img/googleplus.png +0 -0
  119. /package/{src → lib}/fauxton/dashboard.assets/img/linkedin.png +0 -0
  120. /package/{src → lib}/fauxton/dashboard.assets/img/loader.gif +0 -0
  121. /package/{src → lib}/fauxton/dashboard.assets/img/twitter.png +0 -0
  122. /package/{src → lib}/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +0 -0
  123. /package/{src → lib}/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +0 -0
  124. /package/{src → lib}/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +0 -0
  125. /package/{src → lib}/fauxton/index.html +0 -0
package/src/selfhost.ts DELETED
@@ -1,1104 +0,0 @@
1
- /*
2
- * MIT License (MIT)
3
- * Copyright (c) 2018 Activeledger
4
- *
5
- * Permission is hereby granted, free of charge, to any person obtaining a copy
6
- * of this software and associated documentation files (the "Software"), to deal
7
- * in the Software without restriction, including without limitation the rights
8
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- * copies of the Software, and to permit persons to whom the Software is
10
- * furnished to do so, subject to the following conditions:
11
- *
12
- * The above copyright notice and this permission notice shall be included in all
13
- * copies or substantial portions of the Software.
14
- *
15
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- * SOFTWARE.
22
- */
23
- import * as http from "http";
24
- import * as path from "path";
25
- import { promises as fsPromises } from "fs";
26
- import { randomUUID } from "crypto";
27
- import { ActiveHttpd, IActiveHttpIncoming } from "@activeledger/httpd";
28
- import { LevelMe } from "./levelme";
29
- //import { Socket } from "net";
30
- import { SSE } from "./sse";
31
- import { ActiveLogger } from "@activeledger/activelogger";
32
- import { IActiveHttpResponse } from "@activeledger/httpd/lib/httpd";
33
-
34
- (function () {
35
- const fauxtonCache: { [key: string]: { mime: string; data: Buffer } } = {};
36
- // Fauxton Path
37
- const FAUXTON_PATH = __dirname + "/fauxton/";
38
-
39
- // Directory Prefix
40
- const DIR_PREFIX = "./" + process.argv[2] + "/";
41
-
42
- // Listening Port
43
- const PORT = process.argv[3];
44
-
45
- // Data Storage Engine Provider, level provides backwards compatiblility
46
- const DS_PROVIDER = process.argv[4] || "level";
47
-
48
- // Which interface to listen on. This store has no authentication of any
49
- // kind - _bulk_docs will set any document to any revision, DELETE removes
50
- // a stream or the whole database - so it should be reachable only by the
51
- // node that owns it unless someone has deliberately decided otherwise.
52
- //
53
- // Local by default. An SSH tunnel still works (it connects from on the
54
- // host), and so does a container sharing the node's network namespace,
55
- // which is how the gateways reach it. Set db.selfhost.host to widen it.
56
- const BIND_HOST = process.argv[5] || "127.0.0.1";
57
-
58
- // Database Connection Cache
59
- let dbCache: { [index: string]: LevelMe } = {};
60
-
61
- // Track if we are shutting down
62
- let isShuttingDown = false;
63
-
64
- /**
65
- * Graceful Shutdown
66
- *
67
- * @param {ActiveHttpd} httpd host
68
- */
69
- const terminate = async (host: ActiveHttpd) => {
70
- if (!isShuttingDown) {
71
- isShuttingDown = true;
72
- host.shutdown();
73
-
74
- // Close all databases
75
- const dbNames = Object.keys(dbCache);
76
- for (const name of dbNames) {
77
- try {
78
- await dbCache[name].close();
79
- ActiveLogger.info(`Closed database: ${name}`);
80
- } catch (err) {
81
- ActiveLogger.error(`Error closing database ${name}:`, err);
82
- }
83
- }
84
- process.exit(0);
85
- }
86
- }
87
-
88
- /**
89
- * Manages Pouch Connections
90
- *
91
- * @param {string} name
92
- * @returns
93
- */
94
- const getDB = (name: string): LevelMe => {
95
- // Must start with activeledger (will cause problem with config need to resolve)
96
- if (!name.startsWith("activeledger")) {
97
- throw new Error(`invalid database - ${name}`);
98
- }
99
- if (!dbCache[name]) {
100
- try {
101
- dbCache[name] = new LevelMe(DIR_PREFIX, name, DS_PROVIDER);
102
- } catch (err) {
103
- throw err;
104
- }
105
- } else {
106
- }
107
- return dbCache[name];
108
- };
109
-
110
- /**
111
- * Fast uuidV4 Generator
112
- * Credit : https://gist.github.com/jed/982883
113
- *
114
- * @param {number} [a]
115
- * @returns {number}
116
- */
117
- const uuidGenV4 = (): string => randomUUID();
118
-
119
- /**
120
- * Checks for the existant of _id if not ads it and returns
121
- *
122
- * @param {*} doc
123
- * @returns {*}
124
- */
125
- const checkDoc = (doc: any): any => {
126
- if (!doc._id) doc._id = uuidGenV4();
127
- return doc;
128
- };
129
-
130
- // Create Light Server
131
- let http = new ActiveHttpd();
132
-
133
- // Attempt better shutdown
134
- process.on("SIGINT", () => {
135
- terminate(http);
136
- });
137
- process.on("SIGTERM", () => {
138
- terminate(http);
139
- });
140
- process.on("SIGQUIT", () => {
141
- terminate(http);
142
- });
143
-
144
-
145
- // A hand-rolled PID-file "already running" check used to live here:
146
- // record our PID on start, and on the next start, if a previous PID file
147
- // exists and `process.kill(pid, 0)` finds *any* live process with that
148
- // PID, refuse to start. That's unreliable the moment PIDs get reused,
149
- // which happens on every container restart (each gets a fresh, small PID
150
- // namespace) and eventually on bare metal too - a stale recorded PID
151
- // just needs to coincide with some unrelated process for this to produce
152
- // a false "Instance already running" that no amount of restarting fixes,
153
- // since the file was also never cleaned up except on a graceful
154
- // SIGINT/SIGTERM/SIGQUIT (see terminate() above) - a SIGKILL or crash
155
- // left it behind for good. Reproduced live: two nodes' storage refused
156
- // to restart after being recreated mid-session, in full isolation, with
157
- // no other process touching the same data - deleting the stale PID file
158
- // was the only fix.
159
- //
160
- // Removed rather than patched to clean up more reliably: LevelDB (via
161
- // LevelMe below) already takes a real OS-level advisory lock on its own
162
- // LOCK file per database when opened, which the kernel releases
163
- // automatically no matter how the holding process dies - no cleanup
164
- // logic needed, and it actually enforces single-writer correctness
165
- // instead of approximating it by PID. This custom check was redundant
166
- // with that at best, actively wrong at worst.
167
-
168
- http.use("/", "GET", () => {
169
- return {
170
- activeledger: "Welcome to Activeledger data!",
171
- adapters: ["levelme"],
172
- engine: DS_PROVIDER,
173
- };
174
- });
175
-
176
- // Standard Session
177
- http.use("_session", "GET", () => {
178
- return { ok: true, userCtx: { name: null, roles: ["_admin"] } };
179
- });
180
-
181
- // List all databases
182
- http.use("_all_dbs", "GET", async () => {
183
- // Check to see if it is a directory and not a special directory
184
- const files = await fsPromises.readdir(DIR_PREFIX);
185
- // Same pattern deleteDb() below already uses - stat every entry
186
- // concurrently instead of one at a time.
187
- const stats = await Promise.all(
188
- files.map((file) => fsPromises.lstat(DIR_PREFIX + file))
189
- );
190
- const dbs = files.filter(
191
- (file, i) =>
192
- stats[i].isDirectory() &&
193
- file !== process.argv[2] &&
194
- file !== "pouch__all_dbs__" &&
195
- file !== "_replicator" &&
196
- !file.includes("-mrview-")
197
- );
198
-
199
- // Return directories
200
- return dbs;
201
- });
202
-
203
- // Get Database Info
204
- http.use("*", "GET", async (incoming: IActiveHttpIncoming) => {
205
- // Get Database
206
- let db = getDB(incoming.url[0]);
207
- let info = await db.info();
208
-
209
- // Now add data_size
210
- const files = await fsPromises.readdir(DIR_PREFIX + incoming.url[0]);
211
- const stats = await Promise.all(
212
- files.map((source) =>
213
- fsPromises.stat(DIR_PREFIX + incoming.url[0] + "/" + source)
214
- )
215
- );
216
- info.data_size = stats.reduce((total, stat) => total + stat.size, 0);
217
- return info;
218
- });
219
-
220
- // Create Database
221
- http.use("*", "PUT", async (incoming: IActiveHttpIncoming) => {
222
- // Get Database
223
- let db = getDB(incoming.body.name);
224
-
225
- // Create Database
226
- await db.info();
227
- return { ok: true };
228
- });
229
-
230
- // Delete Database Local Var
231
- const deleteDb = async (dir: string) => {
232
- // Read all files and delete
233
- const files = await fsPromises.readdir(dir);
234
- await Promise.all(files.map(source => fsPromises.unlink(dir + "/" + source)));
235
-
236
- // Delete the folder
237
- await fsPromises.rmdir(dir);
238
- };
239
-
240
- // Delete Database
241
- http.use("*", "DELETE", async (incoming: IActiveHttpIncoming) => {
242
- // Can just delete the folder, It shouldn't have nested so can delete all the files first
243
- let dir = DIR_PREFIX + incoming.url[0];
244
-
245
- // Check Folder
246
- try {
247
- await fsPromises.access(dir);
248
- } catch {
249
- // Directory doesn't exist, so we're good.
250
- return { ok: true };
251
- }
252
- // If we have this db open we need to close it
253
- if (dbCache[incoming.url[0]]) {
254
- await dbCache[incoming.url[0]].close();
255
- delete dbCache[incoming.url[0]];
256
- }
257
- // Delete Database
258
- await deleteDb(dir);
259
- return { ok: true };
260
- });
261
-
262
- // Get UUID
263
- http.use("_uuids", "GET", async (incoming: IActiveHttpIncoming) => {
264
- // uuid holder
265
- let uuids = [];
266
-
267
- // Loop for how many requested
268
- for (let i = 0; i < incoming.query.count; i++) {
269
- uuids.push(uuidGenV4());
270
- }
271
- return { uuids };
272
- });
273
-
274
- // Get all docs from a database
275
- http.use("*/_all_docs", "GET", async (incoming: IActiveHttpIncoming) => {
276
- // Get Database
277
- let db = getDB(incoming.url[0]);
278
- return await db.allDocs(prepareAllDocs(incoming.query));
279
- });
280
-
281
- // Get all docs filtered from a database
282
- http.use("*/_all_docs", "POST", async (incoming: IActiveHttpIncoming) => {
283
- // Get Database
284
- let db = getDB(incoming.url[0]);
285
- return await db.allDocs(
286
- prepareAllDocs(Object.assign(incoming.query, incoming.body))
287
- );
288
- });
289
-
290
- // Bit of a proxy call to focus on umids
291
- http.use("*/umids", "GET", async (incoming: IActiveHttpIncoming) => {
292
- return getTransactionUmids(incoming);
293
- });
294
-
295
- http.use("*/transactions", "GET", async (incoming: IActiveHttpIncoming) => {
296
- return getTransactionUmids(incoming);
297
- });
298
-
299
- // Filters the call to focus on umid transaction (assuming right database!)
300
- const getTransactionUmids = async (incoming: IActiveHttpIncoming) => {
301
- // Get Database
302
- let db = getDB(incoming.url[0]);
303
- // Lets make sure it starts with umid:
304
- if (incoming.query["from"].startsWith("umid:")) {
305
- incoming.query["startkey"] = incoming.query["from"];
306
- } else {
307
- incoming.query["startkey"] = `umid:${incoming.query["from"]}`;
308
- }
309
- const txs = (await db.allDocs(prepareAllDocs(incoming.query))) as any;
310
- for (let i = txs.rows.length; i--;) {
311
- const [timestamp, umid] = txs.rows[i]._id.split(",");
312
- txs.rows[i] = {
313
- umid,
314
- timestamp: timestamp.replace("umid:", ""),
315
- };
316
- }
317
- return txs;
318
- };
319
-
320
- /**
321
- * C/Pouch conversion utility tool for _all_docs
322
- *
323
- * @param {*} options
324
- * @returns
325
- */
326
- const prepareAllDocs = (options: any) => {
327
- // Convert Limit
328
- if (options.limit) {
329
- options.limit = parseInt(options.limit) || null;
330
- }
331
-
332
- // Convert startkey to remove "
333
- if (options.startkey) {
334
- options.startkey = options.startkey.replace(/"/g, "");
335
- }
336
-
337
- // Convert endkey to remove " and keep unicode search
338
- if (options.endkey) {
339
- options.endkey = options.endkey.replace(/"/g, "");
340
- options.endkey = options.endkey.slice(0, -1) + "\u9999";
341
- }
342
-
343
- // Filter prefixes
344
- // Leaving this in even though filtering in permissionsChecker as well
345
- if (options.keys) {
346
- options.keys = options.keys.map((id: string) => filterPrefix(id));
347
- }
348
- return options;
349
- };
350
-
351
- /**
352
- * Reusable Get for mutliple paths
353
- *
354
- * @param {string} dbLoc
355
- * @param {string} path
356
- * @returns {Promise<any>}
357
- */
358
- const genericGet = (dbLoc: string, path: string): Promise<any> => {
359
- // Get Database
360
- return new Promise(async (resolve, reject) => {
361
- // Get Database
362
- const db = getDB(dbLoc);
363
- // Filter
364
- db.get(filterPrefix(decodeURIComponent(path), dbLoc))
365
- .then((doc: unknown) => resolve(doc))
366
- .catch((e: unknown) => {
367
- reject(e);
368
- });
369
- });
370
- };
371
-
372
- /**
373
- * Reusable delete (purge like) for mutliple paths
374
- * TODO: Allow recusive delete docName []
375
- *
376
- * @param {string} dbLoc
377
- * @param {string} path
378
- * @returns {Promise<any>}
379
- */
380
- const genericDelete = (dbLoc: string, docName: string): Promise<any> => {
381
- return new Promise(async (resolve, reject) => {
382
- // Get Database
383
- const db = getDB(dbLoc);
384
- try {
385
- await db.del(filterPrefix(docName, dbLoc));
386
- return resolve({ success: "ok" });
387
- } catch (e) {
388
- return reject(e);
389
- }
390
- });
391
- };
392
-
393
- // TODO : Verify request source
394
- http.use("*/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
395
- return await genericDelete(
396
- incoming.url[0],
397
- decodeURIComponent(incoming.url[1])
398
- );
399
- });
400
-
401
- // Start compacting process
402
- http.use("*/_compact", "GET", async (incoming: IActiveHttpIncoming) => {
403
- const db = getDB(incoming.url[0]);
404
- return await db.compact();
405
- });
406
-
407
- // Direct sequence get
408
- http.use("*/_seq/*", "GET", async (incoming: IActiveHttpIncoming) => {
409
- const db = getDB(incoming.url[0]);
410
- return await db.getSeq(incoming.url[2]);
411
- });
412
-
413
- // Direct sequence delete
414
- http.use("*/_seq/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
415
- const db = getDB(incoming.url[0]);
416
- try {
417
- await db.delSeq([incoming.url[2]]);
418
- return { success: "ok" };
419
- } catch {
420
- throw new Error("Seq Delete Failed");
421
- }
422
- });
423
-
424
- // filename comes straight from the request body here - without this
425
- // check it's an arbitrary file write/read primitive (LevelMe.backup()/
426
- // restore() pass it directly to createWriteStream()/createReadStream()
427
- // with no validation of their own, since they're also called from the
428
- // CLI's --backup/--restore flags where an operator-chosen path outside
429
- // the current directory is legitimate - the restriction belongs here,
430
- // at the boundary where the value is actually untrusted remote input,
431
- // not inside LevelMe itself). Restricting to a bare filename (no path
432
- // separators, no ".." components) blocks traversal and absolute paths
433
- // while still allowing any normal filename.
434
- const isSafeFilename = (filename: unknown): filename is string =>
435
- typeof filename === "string" &&
436
- filename.length > 0 &&
437
- path.basename(filename) === filename;
438
-
439
- http.use("*/_backup", "POST", async (incoming: IActiveHttpIncoming) => {
440
- const filename = incoming.body?.filename;
441
- if (filename !== undefined && !isSafeFilename(filename)) {
442
- throw new Error("Invalid backup filename");
443
- }
444
- getDB(incoming.url[0]).backup(filename);
445
- return { status: "started" };
446
- });
447
-
448
- http.use("*/_restore", "POST", async (incoming: IActiveHttpIncoming) => {
449
- const filename = incoming.body?.filename;
450
- if (!isSafeFilename(filename)) {
451
- throw new Error("Invalid restore filename");
452
- }
453
- getDB(incoming.url[0]).restore(filename);
454
- return { status: "started" };
455
- });
456
-
457
- // Get specific docs from a database
458
- http.use("*/*", "GET", async (incoming: IActiveHttpIncoming) => {
459
- return await genericGet(incoming.url[0], incoming.url[1]);
460
- });
461
-
462
- // Gets raw unparsed document straight from leveldb
463
- // Document Store http://localhost:5259/activeledger/_raw/[document._id]
464
- // Sequence (data) http://localhost:5259/activeledger/_raw/[document._id]@[revision] (revision Example = 10-f0b21ef22ec86f7c6d71c250b4563bba)
465
- http.use("*/_raw/*", "GET", async (incoming: IActiveHttpIncoming) => {
466
- // Getting revision?
467
- const [root, revision] = incoming.url[2].split("@");
468
-
469
- // Get Database
470
- const db = getDB(incoming.url[0]);
471
-
472
- // Get Main Doc
473
- const rootDoc = await db.get(decodeURIComponent(root), true);
474
-
475
- // If getting revision we need sequence
476
- if (revision) {
477
- try {
478
- return await db.getSeq(rootDoc.rev_map[revision]);
479
- } catch (e) {
480
- throw new Error("Revision Fetch Failed");
481
- }
482
- }
483
- return rootDoc;
484
- });
485
-
486
- // Add new / updated document to the database
487
- http.use("*/**", "PUT", async (incoming: IActiveHttpIncoming) => {
488
- // Archive Document?
489
- //await markAsArchived(incoming);
490
- return genericPut(incoming.url[0], incoming.body);
491
- });
492
-
493
- /**
494
- * Detects if root pouch document needs to be archived and archives
495
- *
496
- * @param {IActiveHttpIncoming} incoming
497
- * @returns {Promise<boolean>}
498
- */
499
- // const markAsArchived = async (
500
- // incoming: IActiveHttpIncoming
501
- // ): Promise<boolean> => {
502
- // // No more btree archives, return false for now
503
- // return false;
504
-
505
- // const position = isArchivable(incoming.body._rev);
506
- // if (position) {
507
- // try {
508
- // // Get Database
509
- // const db = getDB(incoming.url[0]);
510
-
511
- // // Raw meta doc
512
- // const metaDoc = await db.get(decodeURIComponent(incoming.url[1]), true);
513
-
514
- // // Make sure archive database exists
515
- // const archDb = await getCreateArchiveDb(incoming.url[0]);
516
-
517
- // // Time to modify!
518
- // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
519
-
520
- // // Prevent clashes
521
- // metaDoc._id += ":" + position;
522
-
523
- // // We could add to existing but then we would archive the archive
524
- // // Write document to archive
525
- // await archDb.post(checkDoc(metaDoc));
526
-
527
- // // Rewrite meta root document
528
- // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
529
- // return true;
530
- // } catch (e) {
531
- // // Any errors continue
532
- // return false;
533
- // }
534
- // }
535
- // return false;
536
- // };
537
-
538
- /**
539
- * Pouchdb Metadoc rewrite (trimming)
540
- *
541
- * @param {*} metaDoc
542
- * @param {string} position
543
- * @returns
544
- */
545
- // const prepareArchiveDoc = (metaDoc: any, position: number) => {
546
- // // Time to modify!
547
- // const newMetaDoc = {
548
- // _id: metaDoc._id,
549
- // rev_tree: [
550
- // {
551
- // // pos: metaDoc.rev_tree[0].pos,
552
- // pos: position - 1, // Get parent starting pos
553
- // ids: getLastInTree(metaDoc.rev_tree[0].ids),
554
- // },
555
- // ],
556
- // rev_map: {
557
- // [metaDoc.winningRev]: metaDoc.rev_map[metaDoc.winningRev],
558
- // },
559
- // winningRev: metaDoc.winningRev,
560
- // deleted: metaDoc.deleted,
561
- // seq: metaDoc.seq,
562
- // };
563
- // return newMetaDoc;
564
- // };
565
-
566
- /**
567
- * Create Archive Database
568
- *
569
- * @param {string} name
570
- * @returns
571
- */
572
- // const getCreateArchiveDb = async (name: string): Promise<LevelMe> => {
573
- // const archDb = getDB(name + "_archive");
574
- // await archDb.info();
575
- // return archDb;
576
- // };
577
-
578
- /**
579
- * Gets the last 2 leafs in the tree (parent, current)
580
- *
581
- * @param {any[]} tree
582
- * @param {any[]} [prevTree]
583
- * @returns {*}
584
- */
585
- // const getLastInTree = (tree: any[], prevTree?: any[]): any => {
586
- // if (tree[2].length) {
587
- // return getLastInTree(tree[2][0], tree);
588
- // }
589
- // return prevTree ? prevTree : tree;
590
- // };
591
-
592
- /**
593
- * Should we Archive the document
594
- *
595
- * @param {string} revString
596
- * @returns {number}
597
- */
598
- // const isArchivable = (revString: string): number => {
599
- // if (revString) {
600
- // const position = parseInt(revString.split("-")[0]);
601
-
602
- // // Set at 300 to have 3 attempts before default 1000 id failure error triggered above
603
- // //return position % 300 === 0 ? position : 0;
604
- // return position % 15 === 0 ? position : 0;
605
- // }
606
- // return 0;
607
- // };
608
-
609
- /**
610
- * Reusable Get for mutliple paths
611
- *
612
- * @param {string} dbLoc
613
- * @param {string} path
614
- * @returns {Promise<any>}
615
- */
616
- const genericPut = async (dbLoc: string, body: string): Promise<any> => {
617
- // Get Database
618
- let db = getDB(dbLoc);
619
- try {
620
- return await db.put(body);
621
- } catch (e) {
622
- e.reason = e.message;
623
- throw e;
624
- }
625
- };
626
-
627
- // Filters any prefix (so they're virtual) (: reserved character) to real stream id
628
- const filterPrefix = (stream: string, db?: string): string => {
629
-
630
- // Only need to filter activeledger, making optional just in case not passed
631
- if (db && db !== 'activeledger') {
632
- return stream;
633
- } else {
634
- // Remove any suffix like :volatile :stream :umid
635
- let [streamId, suffix] = stream.split(":");
636
-
637
- // If id length more than 64 trim the start
638
- if (streamId.length > 64 && !streamId.startsWith("0x")) {
639
- streamId = streamId.slice(-64);
640
- }
641
-
642
- // If suffix add it back to return
643
- if (suffix) {
644
- return streamId + ":" + suffix;
645
- }
646
-
647
- // Return just the id
648
- return streamId;
649
- }
650
- };
651
-
652
- // Add new / updated document to the database with auto id
653
- http.use("*", "POST", async (incoming: IActiveHttpIncoming) => {
654
- // Get Database
655
- let db = getDB(incoming.url[0]);
656
-
657
- // Archive Document?
658
- //await markAsArchived(incoming);
659
-
660
- try {
661
- return await db.post(checkDoc(incoming.body));
662
- } catch (e) {
663
- e.reason = e.message;
664
- throw e;
665
- }
666
- });
667
-
668
- http.use(
669
- "*/events",
670
- "GET",
671
- async (
672
- incoming: IActiveHttpIncoming,
673
- req: http.IncomingMessage,
674
- res: IActiveHttpResponse
675
- ) => {
676
- // Get Database
677
- let db = getDB(incoming.url[0]);
678
-
679
- const sse = new SSE(res);
680
- const lastEventId = req.headers["Last-Event-ID"];
681
-
682
- // Fetch anything newer since then and push
683
- if (lastEventId) {
684
- // Merge with getTransactionUmids as there is little difference
685
- const events = (await db.allDocs(
686
- prepareAllDocs({
687
- startkey: `event:${lastEventId}`,
688
- endkey: `event:`,
689
- include_docs: true,
690
- })
691
- )) as any;
692
- // Start at 1 to skip the event that was already sent
693
- for (let i = 1; i < events.rows.length; i++) {
694
- const id = events.rows[i]._id.replace("event:", "");
695
- delete events.rows[i]._id;
696
- delete events.rows[i]._rev;
697
- sse.write(id, events.rows[i]);
698
- }
699
- }
700
-
701
- // Listener Process event (to turn off)
702
- const listener = (change: any) => {
703
- if (change.id.startsWith("event:")) {
704
- delete change.doc._id;
705
- delete change.doc._rev;
706
- if (!sse.write(change.id.replace("event:", ""), change.doc)) {
707
- cancelChanges();
708
- }
709
- }
710
- };
711
-
712
- // Stop listening for changes
713
- const cancelChanges = () => {
714
- changes.off("change", listener);
715
- };
716
-
717
- // Run on disconnect immediately, rather than only being discovered
718
- // reactively the next time a change event fires and listener() sees
719
- // sse.write() return false - on a quiet database that could be a long
720
- // time (or never) after the client actually disconnects.
721
- sse.onDisconnect(cancelChanges);
722
-
723
- // Listening for changes
724
- let changes = db.changes().on("change", listener);
725
-
726
- return "handled";
727
- }
728
- );
729
-
730
- // Listen for changes on the database
731
- http.use(
732
- "*/_changes",
733
- "GET",
734
- async (
735
- incoming: IActiveHttpIncoming,
736
- req: http.IncomingMessage,
737
- res: IActiveHttpResponse
738
- ) => {
739
- // Get Database
740
- let db = getDB(incoming.url[0]);
741
-
742
- // Limit check
743
- if (incoming.query.limit < 1) {
744
- incoming.query.limit = 1;
745
- }
746
-
747
- // Convert since into number if not now
748
- if (incoming.query.since !== "now") {
749
- incoming.query.since = parseInt(incoming.query.since);
750
- }
751
-
752
- if (
753
- incoming.query.feed === "continuous" ||
754
- incoming.query.feed === "longpoll"
755
- ) {
756
- if (incoming.query.feed === "continuous") {
757
- // Currently we do not do continuous
758
- } else {
759
- // Long polling heartbeat - armed only once the body is actually
760
- // open for writing, further down.
761
- let hBInterval: ReturnType<typeof setInterval> | null = null;
762
-
763
- // Clean up
764
- let cleanUp = () => {
765
- if (hBInterval) {
766
- clearInterval(hBInterval);
767
- hBInterval = null;
768
- }
769
- };
770
-
771
- // `res` here is the RAW uWebSockets.js HttpResponse (see
772
- // IActiveHttpResponse/@activeledger/httpd/src/httpd.ts -
773
- // it constructs a Node-like synthetic object for `req`, but
774
- // passes `res` straight through unwrapped). The `http`/
775
- // `http.ServerResponse` types this file imports for `req`/`res`
776
- // are Node's own - never real here, a holdover from before this
777
- // framework's uWS.js migration. Two real, compounding bugs
778
- // followed from believing them: res.setHeader() doesn't exist on
779
- // a real uWS HttpResponse (a prior "fix", 425ccc6 "make sse work
780
- // again!", replaced a setHeader() call with two res.write() calls
781
- // writing literal "HTTP/1.1 200 OK\r\n\r\n" text into the BODY
782
- // instead - also wrong, since write() there is real and does
783
- // append to the body); and every write from here on happens in a
784
- // LATER tick than the request handler's own synchronous top
785
- // portion (a .then() callback, an EventEmitter listener firing
786
- // whenever a future change occurs) - uWS requires those to be
787
- // wrapped in res.cork(), same as the already-working /events SSE
788
- // handler (sse.ts) already does, or the write silently corrupts/
789
- // fails. None of this was ever caught because BOTH bugs (and two
790
- // more besides - see levelme.ts's since="now" and seq fixes)
791
- // left the response either hanging forever or throwing before
792
- // any client ever got as far as trying to parse a body - so nothing
793
- // ever actually exercised this branch successfully until all of
794
- // them were fixed together.
795
- res.cork(() => {
796
- res.writeHeader("Content-type", ActiveHttpd.mimeType[".json"]);
797
- });
798
-
799
- // Read Type
800
- incoming.query.live = incoming.query.continuous = false;
801
-
802
- db.changesFromSeq(incoming.query).then((complete: any) => {
803
- if (complete.results.length) {
804
- if (res.writable) {
805
- res.cork(() => {
806
- res.write(JSON.stringify(complete));
807
- res.end();
808
- });
809
- }
810
- cleanUp();
811
- } else {
812
- // do the longpolling
813
- // mimicking CouchDB, start sending the JSON immediately
814
- if (res.writable) {
815
- res.cork(() => {
816
- res.write('{"results":[\n');
817
- });
818
- }
819
- incoming.query.live = incoming.query.continuous = true;
820
-
821
- // Keep bytes flowing while nothing is changing.
822
- //
823
- // This write was commented out, and that is what made a quiet
824
- // feed fatal rather than merely quiet. The response holds open
825
- // with nothing on the wire, so the client's undici bodyTimeout
826
- // (300s - ActiveRequest's default, packages/utilities) fires and
827
- // the round comes back with no body. Downstream,
828
- // ActiveDSChanges.listen() dereferenced response.data.results on
829
- // that null and threw, and nothing re-armed the loop - so the
830
- // changes feed died for the lifetime of the process. Every five
831
- // idle minutes was enough. Live-confirmed: nano-gateway kept its
832
- // SSE socket open and heartbeating to its own subscribers the
833
- // whole time, so every health signal said the link was fine
834
- // while no change could ever arrive again. Background push on a
835
- // real device stopped and nothing anywhere reported a fault.
836
- //
837
- // A newline is insignificant whitespace between JSON array
838
- // elements, so it cannot corrupt the body being streamed - this
839
- // is exactly what CouchDB sends. It has to be corked: uWS
840
- // requires it for any write from a later tick, and an uncorked
841
- // one is silently dropped or corrupts the stream, which is the
842
- // likeliest reason this was disabled rather than fixed.
843
- //
844
- // Default is well under the client's timeout so an ordinary
845
- // quiet period never reaches it. The client-side guard in
846
- // dsconnect.ts is still required: this reduces how often a bad
847
- // round happens, it does not make one survivable.
848
- hBInterval = setInterval(() => {
849
- if (!res.writable) {
850
- // The client went away mid-longpoll. Without this the
851
- // interval and the change listener both leak for the
852
- // lifetime of the process, one per abandoned request.
853
- cleanUp();
854
- cancelChanges();
855
- return;
856
- }
857
- res.cork(() => {
858
- res.write("\n");
859
- });
860
- }, Number(incoming.query.heartbeat) || 30000);
861
-
862
- // Listener Process event (to turn off)
863
- // A longpoll round returns EVERY pending change, not one.
864
- //
865
- // bulkDocs emits one "change" per document in a synchronous
866
- // loop (levelme.ts). The previous listener responded to the
867
- // FIRST emit by writing it, ending the response and detaching
868
- // itself - all synchronously - so every remaining document in
869
- // the same commit found no listener at all. With no sequence
870
- // backfill (changesFromSeq can never return rows, see its own
871
- // comment) those changes were gone permanently: `since` moved
872
- // past them and nothing could replay them.
873
- //
874
- // For a ledger that means an ordinary multi-stream transaction -
875
- // a transfer touching sender and recipient - notified about one
876
- // of them. Which one was arbitrary: levelme emits backwards, so
877
- // it was the LAST document written. Downstream that reads as an
878
- // intermittent push bug rather than as data loss.
879
- //
880
- // So changes are accumulated and flushed on the next tick. Every
881
- // emit from one commit is synchronous, so a single setImmediate
882
- // catches the whole batch without waiting for anything.
883
- let pending: any[] = [];
884
- let flushScheduled = false;
885
-
886
- const flush = () => {
887
- try {
888
- if (!res.writable || pending.length === 0) return;
889
- const batch = pending;
890
- pending = [];
891
- // The highest seq in the batch, not the last one written -
892
- // levelme emits in reverse order, so "last" is the oldest.
893
- const lastSeq = batch.reduce(
894
- (max: number, c: any) => (Number(c.seq) > max ? Number(c.seq) : max),
895
- 0
896
- );
897
- // res.end() is load-bearing, not tidiness. Both the write
898
- // and the end used to be commented out, so a client whose
899
- // HTTP layer waits for the response to COMPLETE before
900
- // resolving - ActiveRequest/axios does - hung forever the
901
- // moment a real change occurred, even though the bytes were
902
- // already on the socket. Live-confirmed: it is why a
903
- // nano-gateway subscriber connected fine, kept heartbeating,
904
- // and never received a push for a committed change.
905
- res.cork(() => {
906
- res.write(batch.map((c: any) => JSON.stringify(c)).join(",\n"));
907
- res.write('],\n"last_seq":' + lastSeq + "}\n");
908
- res.end();
909
- });
910
- } finally {
911
- // In a finally so a throw while writing - an unserialisable
912
- // doc, a discarded uWS response - cannot leak the heartbeat
913
- // interval and the change listener for the process lifetime.
914
- cleanUp();
915
- cancelChanges();
916
- }
917
- };
918
-
919
- const listener = (change: any) => {
920
- // Deliberately does no writability check of its own - flush()
921
- // does it once, for the whole batch, immediately before
922
- // writing. (For the record on `res.writable`: it is the real,
923
- // framework-maintained "is this connection still open" flag,
924
- // unlike req.connection.destroyed, which this handler used to
925
- // consult and which was always undefined - the httpd
926
- // framework's synthetic `req` carries only
927
- // connection.remoteAddress, so that check never gated anything
928
- // and never threw. sse.ts relies on res.writable too.)
929
- pending.push(change);
930
- if (!flushScheduled) {
931
- flushScheduled = true;
932
- setImmediate(flush);
933
- }
934
- };
935
-
936
-
937
- // Stop listening for changes
938
- const cancelChanges = () => {
939
- changes.off("change", listener);
940
- };
941
-
942
- // Listening for changes
943
- let changes = db.changes().on("change", listener);
944
- }
945
- }).catch((error: unknown) => {
946
- // Previously unhandled - a rejection here (e.g. a malformed
947
- // `since`) surfaced only as an opaque 500 with an empty {}
948
- // body and no indication anything had actually gone wrong
949
- // server-side, all the way up through whatever HTTP client hit
950
- // this route. Finalise the response with a real error instead
951
- // of leaving it hanging or silently swallowed.
952
- if (res.writable) {
953
- res.cork(() => {
954
- res.writeStatus("500");
955
- res.write(JSON.stringify({ error: String(error) }));
956
- res.end();
957
- });
958
- }
959
- cleanUp();
960
- });
961
- }
962
- return "handled";
963
- } else {
964
- // Just get the latest
965
- return await db.changesFromSeq(incoming.query);
966
- }
967
- }
968
- );
969
-
970
- // Add new / updated BULK document to the database
971
- http.use("*/_bulk_docs", "POST", async (incoming: IActiveHttpIncoming) => {
972
- // Can no longer be an array
973
- if (!Array.isArray(incoming.body)) {
974
- // Must have docs
975
- if (incoming.body.docs) {
976
- // Get Database
977
- let db = getDB(incoming.url[0]);
978
-
979
- // Prepare Archive database
980
- // Make sure archive database exists
981
- //const archDb = await getCreateArchiveDb(incoming.url[0]);
982
-
983
- // We need to see if the docs need archiving
984
- // for (let i = incoming.body.docs.length; i--; ) {
985
- // const doc = incoming.body.docs[i];
986
- // const position = isArchivable(doc._rev);
987
- // if (position) {
988
- // try {
989
- // // Raw meta doc
990
- // const metaDoc = await db.get(doc._id, true);
991
-
992
- // // Time to modify!
993
- // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
994
-
995
- // // Prevent clashes
996
- // metaDoc._id += ":" + position;
997
-
998
- // // We could add to existing but then we would archive the archive
999
- // // Write document to archive
1000
- // await archDb.post(checkDoc(metaDoc));
1001
-
1002
- // // Rewrite meta root document
1003
- // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
1004
- // } catch (e) {
1005
- // // Ignore errors and continue
1006
- // }
1007
- // }
1008
- // }
1009
-
1010
- // Bulk Insert
1011
- if (await db.bulkDocs(incoming.body.docs, incoming.body.options)) {
1012
- return {
1013
- ok: true,
1014
- };
1015
- } else {
1016
- return {
1017
- ok: false,
1018
- };
1019
- }
1020
- }
1021
- }
1022
- });
1023
-
1024
- // Find API
1025
- http.use("*/_find", "POST", async (incoming: IActiveHttpIncoming) => {
1026
- return { error: "find is deprecated" };
1027
- });
1028
-
1029
- // Explain
1030
- http.use("*/_explain", "POST", async (incoming: IActiveHttpIncoming) => {
1031
- return { error: "explain is deprecated" };
1032
- });
1033
-
1034
- // Fauxton
1035
- const fauxton = async (
1036
- incoming: IActiveHttpIncoming,
1037
- req: {
1038
- headers: {
1039
- [index: string]: string;
1040
- };
1041
- method: string;
1042
- url: string;
1043
- connection: {
1044
- remoteAddress: string;
1045
- };
1046
- },
1047
- res: IActiveHttpResponse
1048
- ) => {
1049
- // We want to force /_utils to /_utils/ as this is the CouchDB behavior
1050
- if (req.url === "/_utils") {
1051
- // res.writeHead(301, {
1052
- // Location: "/_utils/",
1053
- // });
1054
- // return;
1055
- req.url = "/_utils/";
1056
- }
1057
-
1058
- // File to send
1059
- let file = FAUXTON_PATH + "/index.html";
1060
-
1061
- // If path is not default overwrite
1062
- if (req.url !== "/_utils/") {
1063
- // Resolved and contained, not concatenated. uWebSockets hands the
1064
- // path through as sent, so "../" segments arrive verbatim - and
1065
- // FAUXTON_PATH sits several levels inside the install, so a few of
1066
- // them reach the node's working directory, where config.json and the
1067
- // .identity private key live.
1068
- const requested = path.resolve(
1069
- FAUXTON_PATH,
1070
- "." + decodeURIComponent((req.url as string).replace("/_utils", ""))
1071
- );
1072
-
1073
- if (!requested.startsWith(path.resolve(FAUXTON_PATH) + path.sep)) {
1074
- // Outside the asset directory. Serve the app shell rather than
1075
- // reporting whether the path existed.
1076
- return fauxtonCache[FAUXTON_PATH + "/index.html"] || null;
1077
- }
1078
-
1079
- file = requested;
1080
- }
1081
-
1082
- if (fauxtonCache[file]) {
1083
- return fauxtonCache[file];
1084
- }
1085
-
1086
- try {
1087
- await fsPromises.access(file);
1088
- const data = await fsPromises.readFile(file);
1089
- const response = {
1090
- mime: `${ActiveHttpd.mimeType[path.parse(file).ext] || "text/plain"}`,
1091
- data: data as Buffer,
1092
- }
1093
- fauxtonCache[file] = response;
1094
- return response;
1095
- } catch {
1096
- // Not found, will result in a 404 from httpd
1097
- }
1098
- };
1099
- http.use("_utils", "GET", fauxton);
1100
- http.use("_utils/**", "GET", fauxton);
1101
-
1102
- // Start Server
1103
- http.listen(parseInt(PORT), false, BIND_HOST);
1104
- })();