@serve.zone/dcrouter 34.1.0 → 34.2.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '34.1.0',
6
+ version: '34.2.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "34.1.0",
4
+ "version": "34.2.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "@git.zone/tsrepo": "^1.1.1",
21
21
  "@push.rocks/smartdb-v2": "npm:@push.rocks/smartdb@2.14.6",
22
22
  "@push.rocks/smartdb-v5.2": "npm:@push.rocks/smartdb@5.2.2",
23
+ "@serve.zone/dcrouter-v33": "npm:@serve.zone/dcrouter@33.0.0",
23
24
  "@types/dns-packet": "^5.6.5",
24
25
  "@types/node": "26.6.5",
25
26
  "@types/web-push": "3.6.4",
@@ -50,7 +51,7 @@
50
51
  "@push.rocks/smartlog": "^3.2.2",
51
52
  "@push.rocks/smartmcp": "^0.3.0",
52
53
  "@push.rocks/smartmetrics": "^3.0.3",
53
- "@push.rocks/smartmigration": "2.0.1",
54
+ "@push.rocks/smartmigration": "2.1.0",
54
55
  "@push.rocks/smartmta": "^13.0.1",
55
56
  "@push.rocks/smartnetwork": "^4.10.1",
56
57
  "@push.rocks/smartpath": "^6.0.0",
@@ -70,6 +71,7 @@
70
71
  "@tsclass/tsclass": "^9.5.1",
71
72
  "@types/qrcode": "^1.5.6",
72
73
  "argon2": "^0.45.1",
74
+ "bson": "7.3.2",
73
75
  "ipaddr.js": "^2.5.0",
74
76
  "lru-cache": "^11.5.2",
75
77
  "qrcode": "^1.5.4",
package/readme.md CHANGED
@@ -63,6 +63,31 @@ Use the package as a TypeScript library:
63
63
  pnpm add @serve.zone/dcrouter
64
64
  ```
65
65
 
66
+ ## CLI
67
+
68
+ ```bash
69
+ dcrouter # start the router (DCROUTER_MODE=OCI_CONTAINER for the container configuration)
70
+ dcrouter --version
71
+ dcrouter --help
72
+ dcrouter migrate --storage-path <absolute root> --to <version> [--db-name dcrouter] [--plan]
73
+ ```
74
+
75
+ `dcrouter` starts the router only without arguments; from 34.2.0 on it refuses any argument it does not know (exit code 2) and starts nothing. Releases up to 34.1.0 started the router on an unknown argument, so check `dcrouter --version` before running a command a later release added.
76
+
77
+ `dcrouter migrate` walks the data of a **stopped** dcrouter back to an earlier data version through the migration steps' down handlers; forward migrations happen when dcrouter starts. The supported downgrade is data version `34.0.0` → `32.6.0`, the data dcrouter 33.0.0 reads, which returns the route DNS ownership the `release-annexed-gateway-route-dns-records` step released:
78
+
79
+ ```bash
80
+ # the embedded database's default root is ~/.serve.zone/dcrouter/tsmdb (dbConfig.storagePath overrides it)
81
+ dcrouter migrate --storage-path /root/.serve.zone/dcrouter/tsmdb --to 32.6.0 --plan # report only
82
+ dcrouter migrate --storage-path /root/.serve.zone/dcrouter/tsmdb --to 32.6.0
83
+ ```
84
+
85
+ - `--storage-path` (required, absolute, no default) is the embedded database's storage root; `--db-name` is the database inside it (default `dcrouter`). The root is checked offline first and the engine's owner lock is taken, so a root a running dcrouter owns is refused.
86
+ - `--to` must be a step boundary behind the ledger. A forward target, a ledger a newer dcrouter wrote and a database without a ledger are refused; a target equal to the ledger is a no-op.
87
+ - A database that dcrouter 34.0.0 or 34.1.0 upgraded is refused: those releases did not record the ownership they released, so restore the export taken before that upgrade instead. Upgrade a 33.0.0 database straight to 34.2.0 or later to keep the way back.
88
+ - Records written after the upgrade are kept: a record deleted since is not recreated, an edited one gets its ownership back, and any record whose ownership conflicts with the recorded one refuses the whole downgrade with nothing changed.
89
+ - The last output line is `DCROUTER_MIGRATE_RESULT:{json}` (`ok`, the versions and the steps, or the error); the exit code is 0, 1 on a refusal or failure, and 2 on a command line it does not accept.
90
+
66
91
  ## Quick Start
67
92
 
68
93
  This starts the gateway on unprivileged ports and stores data under the default `~/.serve.zone/dcrouter` base directory.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '34.1.0',
6
+ version: '34.2.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
package/ts/index.ts CHANGED
@@ -7,6 +7,7 @@ export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from
7
7
 
8
8
  // DcRouter
9
9
  import { DcRouter } from './classes.dcrouter.js';
10
+ import { runMigrateCommand } from './migrate-command.js';
10
11
  export * from './classes.dcrouter.js';
11
12
 
12
13
  // RADIUS module
@@ -21,27 +22,44 @@ export * from './webpush/index.js';
21
22
  // HTTP/3 module
22
23
  export type { IHttp3Config } from './http3/index.js';
23
24
 
25
+ const cliUsage = `Usage:
26
+ dcrouter
27
+ dcrouter --version
28
+ dcrouter --help
29
+ dcrouter migrate --storage-path <absolute path> --to <version> [--db-name <name>] [--plan]
30
+
31
+ Environment:
32
+ DCROUTER_MODE=OCI_CONTAINER Start with OCI container configuration
33
+ DCROUTER_DNS_BIND_INTERFACE Override the embedded DNS UDP bind address
34
+ DATA_DIR=<path> Override the writable dcrouter data directory
35
+ `;
36
+
24
37
  export const runCli = async () => {
25
38
  const args = process.argv.slice(2);
26
39
 
40
+ if (args[0] === 'migrate') {
41
+ process.exitCode = await runMigrateCommand(args.slice(1));
42
+ return;
43
+ }
44
+
27
45
  if (args.includes('--version') || args.includes('version')) {
28
46
  console.log(commitinfo.version);
29
47
  return;
30
48
  }
31
49
 
32
50
  if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
33
- console.log(`dcrouter ${commitinfo.version}
34
-
35
- Usage:
36
- dcrouter
37
- dcrouter --version
38
- dcrouter --help
51
+ console.log(`dcrouter ${commitinfo.version}\n\n${cliUsage}`);
52
+ return;
53
+ }
39
54
 
40
- Environment:
41
- DCROUTER_MODE=OCI_CONTAINER Start with OCI container configuration
42
- DCROUTER_DNS_BIND_INTERFACE Override the embedded DNS UDP bind address
43
- DATA_DIR=<path> Override the writable dcrouter data directory
44
- `);
55
+ // Only a bare `dcrouter` starts the router: an argument this release does not know (a command
56
+ // a later release added, a typo) must never start it against the data it was meant for.
57
+ if (args.length > 0) {
58
+ console.error(
59
+ `dcrouter: unknown argument${args.length > 1 ? 's' : ''} ${args.map((argArg) => `"${argArg}"`).join(' ')}; `
60
+ + `nothing was started.\n\n${cliUsage}`,
61
+ );
62
+ process.exitCode = 2;
45
63
  return;
46
64
  }
47
65
 
@@ -0,0 +1,198 @@
1
+ import * as plugins from './plugins.js';
2
+ import { createDowngradeMigrationRunner, DcRouterMigrationError } from '../ts_migrations/index.js';
3
+ import type { IDowngradeMigrationRunResult } from '../ts_migrations/index.js';
4
+
5
+ /** The prefix of the one line `dcrouter migrate` ends with: the outcome as JSON. */
6
+ export const migrateResultLinePrefix = 'DCROUTER_MIGRATE_RESULT:';
7
+
8
+ export const migrateCommandUsage = `Usage:
9
+ dcrouter migrate --storage-path <absolute path> --to <version> [--db-name <name>] [--plan]
10
+
11
+ Walks the data of a stopped dcrouter back to an earlier data version, running each
12
+ step's down handler. Forward migrations happen when dcrouter starts.
13
+
14
+ --storage-path <path> The embedded database's storage root (absolute; no default)
15
+ --to <version> The data version to walk back to, a step boundary
16
+ --db-name <name> The database inside the root (default: dcrouter)
17
+ --plan Report the steps the downgrade would run, change nothing
18
+
19
+ The root must not be in use: the command refuses a root a running dcrouter owns.
20
+ The last line of the output is ${migrateResultLinePrefix}{json}.
21
+ `;
22
+
23
+ /** A command line `dcrouter migrate` refuses before it opens anything. */
24
+ export class MigrateCommandUsageError extends Error {
25
+ constructor(messageArg: string) {
26
+ super(messageArg);
27
+ this.name = 'MigrateCommandUsageError';
28
+ }
29
+ }
30
+
31
+ export interface IMigrateCommandOptions {
32
+ storagePath: string;
33
+ targetVersion: string;
34
+ dbName: string;
35
+ planOnly: boolean;
36
+ }
37
+
38
+ export type TMigrateCommandResult =
39
+ | ({ ok: true; plan: boolean; storagePath: string; dbName: string } & IDowngradeMigrationRunResult)
40
+ | { ok: false; error: { name: string; code?: string; message: string } };
41
+
42
+ /**
43
+ * The stopped-root check's bounds: the largest every input admits (smartdb readme, "Offline
44
+ * Physical Namespace Inspection"). A root over any of them is refused, never partly inspected: a
45
+ * data file over `maximumDataFileBytes` fails the whole inspection with `ERESOURCE_LIMIT`
46
+ * (nosqldb 5.7.1 `rustdb-storage/src/offline_inspection/physical_namespace_inspection.rs:760`).
47
+ */
48
+ export const offlineInspectionLimits: plugins.smartdb.ILocalSmartDbOfflinePhysicalNamespaceInspectionLimits = {
49
+ maximumEntries: 100_000,
50
+ maximumDatabases: 4_096,
51
+ maximumCollections: 10_000,
52
+ maximumIndexes: 100_000,
53
+ maximumDataFileBytes: 268_435_456,
54
+ maximumWalFileBytes: 67_108_864,
55
+ maximumIndexFileBytes: 16_777_216,
56
+ maximumTotalFileBytes: 1_099_511_627_776,
57
+ maximumRecordsPerCollection: 1_000_000,
58
+ maximumRecordBytes: 17_825_792,
59
+ maximumResultBytes: 16_777_216,
60
+ };
61
+ const offlineInspectionTimeoutMs = 10 * 60_000;
62
+
63
+ export function parseMigrateArguments(argsArg: string[]): IMigrateCommandOptions {
64
+ let storagePath: string | undefined;
65
+ let targetVersion: string | undefined;
66
+ let dbName = 'dcrouter';
67
+ let planOnly = false;
68
+ const valueOf = (indexArg: number, flagArg: string) => {
69
+ const value = argsArg[indexArg + 1];
70
+ if (value === undefined || value.startsWith('--')) {
71
+ throw new MigrateCommandUsageError(`${flagArg} needs a value.`);
72
+ }
73
+ return value;
74
+ };
75
+ for (let index = 0; index < argsArg.length; index++) {
76
+ const arg = argsArg[index];
77
+ switch (arg) {
78
+ case '--storage-path':
79
+ storagePath = valueOf(index++, arg);
80
+ break;
81
+ case '--to':
82
+ targetVersion = valueOf(index++, arg);
83
+ break;
84
+ case '--db-name':
85
+ dbName = valueOf(index++, arg);
86
+ break;
87
+ case '--plan':
88
+ planOnly = true;
89
+ break;
90
+ default:
91
+ throw new MigrateCommandUsageError(`Unknown argument "${arg}".`);
92
+ }
93
+ }
94
+ if (!storagePath) throw new MigrateCommandUsageError('--storage-path is required.');
95
+ if (!plugins.path.isAbsolute(storagePath)) {
96
+ throw new MigrateCommandUsageError(`--storage-path must be an absolute path, not "${storagePath}".`);
97
+ }
98
+ if (!targetVersion) throw new MigrateCommandUsageError('--to is required.');
99
+ return { storagePath: plugins.path.resolve(storagePath), targetVersion, dbName, planOnly };
100
+ }
101
+
102
+ /**
103
+ * Runs the downgrade offline: a stopped-root check first, then the engine, whose owner lock refuses
104
+ * a root a running dcrouter holds, then the reverse migration. Both handles are released before it
105
+ * returns, whatever happened.
106
+ */
107
+ export async function runMigrateCommandOptions(optionsArg: IMigrateCommandOptions): Promise<TMigrateCommandResult> {
108
+ // Refuses a root an engine holds (the sidecar takes the shared owner lock), a root that is not a
109
+ // current SmartDB root, and any file that does not validate, before the engine is started.
110
+ const inspection = await plugins.smartdb.LocalSmartDb.inspectOfflinePhysicalNamespaces(
111
+ { folderPath: optionsArg.storagePath, limits: offlineInspectionLimits },
112
+ { timeoutMs: offlineInspectionTimeoutMs },
113
+ );
114
+ const databaseNames = inspection.databases.map((databaseArg) => databaseArg.name);
115
+ if (!databaseNames.includes(optionsArg.dbName)) {
116
+ throw new DcRouterMigrationError(
117
+ `The storage root holds no database "${optionsArg.dbName}" (it holds ${databaseNames.length > 0
118
+ ? databaseNames.map((nameArg) => `"${nameArg}"`).join(', ')
119
+ : 'none'}). Nothing was changed.`,
120
+ );
121
+ }
122
+
123
+ const localSmartDb = new plugins.smartdb.LocalSmartDb({ folderPath: optionsArg.storagePath });
124
+ let db: plugins.smartdata.SmartdataDb | undefined;
125
+ let failure: { error: unknown } | undefined;
126
+ try {
127
+ const { connectionUri } = await localSmartDb.start();
128
+ db = new plugins.smartdata.SmartdataDb({ mongoDbUrl: connectionUri, mongoDbName: optionsArg.dbName });
129
+ await db.init();
130
+ const runner = await createDowngradeMigrationRunner(db, optionsArg.targetVersion);
131
+ const result = optionsArg.planOnly ? await runner.plan() : await runner.run();
132
+ return {
133
+ ok: true,
134
+ plan: optionsArg.planOnly,
135
+ storagePath: optionsArg.storagePath,
136
+ dbName: optionsArg.dbName,
137
+ currentVersionBefore: result.currentVersionBefore,
138
+ currentVersionAfter: result.currentVersionAfter,
139
+ targetVersion: result.targetVersion,
140
+ wasUpToDate: result.wasUpToDate,
141
+ wasFreshInstall: result.wasFreshInstall,
142
+ totalDurationMs: result.totalDurationMs,
143
+ stepsApplied: result.stepsApplied.map(({ id, fromVersion, toVersion, status }) => ({ id, fromVersion, toVersion, status })),
144
+ stepsSkipped: result.stepsSkipped.map(({ id, fromVersion, toVersion, status }) => ({ id, fromVersion, toVersion, status })),
145
+ };
146
+ } catch (error: unknown) {
147
+ failure = { error };
148
+ throw error;
149
+ } finally {
150
+ // Both handles are released whatever happened. A cleanup failure after a failed migration is
151
+ // reported beside it, and the migration's own error stays the one the command answers with.
152
+ const cleanupErrors: unknown[] = [];
153
+ for (const release of [() => db?.close(), () => localSmartDb.stop()]) {
154
+ try {
155
+ await release();
156
+ } catch (cleanupError: unknown) {
157
+ cleanupErrors.push(cleanupError);
158
+ }
159
+ }
160
+ if (cleanupErrors.length > 0 && !failure) {
161
+ throw new AggregateError(cleanupErrors, 'dcrouter migrate could not release the database');
162
+ }
163
+ for (const cleanupError of failure ? cleanupErrors : []) {
164
+ console.error(`dcrouter migrate: releasing the database also failed: ${describeMigrateError(cleanupError).message}`);
165
+ }
166
+ }
167
+ }
168
+
169
+ const describeMigrateError = (errorArg: unknown): { name: string; code?: string; message: string } => {
170
+ if (!(errorArg instanceof Error)) return { name: 'Error', message: String(errorArg) };
171
+ const code = (errorArg as Error & { code?: unknown }).code;
172
+ return {
173
+ name: errorArg.name,
174
+ ...(typeof code === 'string' ? { code } : {}),
175
+ message: errorArg.message,
176
+ };
177
+ };
178
+
179
+ /** `dcrouter migrate …`; resolves to the process exit code and prints the result line last. */
180
+ export async function runMigrateCommand(argsArg: string[]): Promise<number> {
181
+ if (argsArg.includes('--help') || argsArg.includes('-h')) {
182
+ console.log(migrateCommandUsage);
183
+ return 0;
184
+ }
185
+ let result: TMigrateCommandResult;
186
+ let exitCode: number;
187
+ try {
188
+ result = await runMigrateCommandOptions(parseMigrateArguments(argsArg));
189
+ exitCode = 0;
190
+ } catch (error: unknown) {
191
+ result = { ok: false, error: describeMigrateError(error) };
192
+ exitCode = error instanceof MigrateCommandUsageError ? 2 : 1;
193
+ console.error(`dcrouter migrate: ${result.error.message}`);
194
+ if (error instanceof MigrateCommandUsageError) console.error(migrateCommandUsage);
195
+ }
196
+ console.log(`${migrateResultLinePrefix}${JSON.stringify(result)}`);
197
+ return exitCode;
198
+ }
package/ts/plugins.ts CHANGED
@@ -115,6 +115,7 @@ export {
115
115
 
116
116
  // third party
117
117
  import * as argon2 from 'argon2';
118
+ import * as bson from 'bson';
118
119
  import * as lruCache from 'lru-cache';
119
120
  import ipaddr from 'ipaddr.js';
120
121
  import * as uuid from 'uuid';
@@ -122,6 +123,7 @@ import webpush from 'web-push';
122
123
 
123
124
  export {
124
125
  argon2,
126
+ bson,
125
127
  lruCache,
126
128
  ipaddr,
127
129
  uuid,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '34.1.0',
6
+ version: '34.2.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }