@sfmc-bds/db-server 0.1.0 → 0.2.0-beta.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.
- package/README.md +1 -1
- package/dist/db-tables.js +16 -17
- package/dist/db-tables.js.map +7 -1
- package/dist/domain/bridge.js +54 -77
- package/dist/domain/bridge.js.map +7 -1
- package/dist/domain/economy.d.ts +83 -2
- package/dist/domain/economy.d.ts.map +1 -1
- package/dist/domain/economy.js +292 -281
- package/dist/domain/economy.js.map +7 -1
- package/dist/domain/schema.js +480 -426
- package/dist/domain/schema.js.map +7 -1
- package/dist/domain/transaction.js +32 -80
- package/dist/domain/transaction.js.map +7 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +85 -84
- package/dist/env.js.map +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +218 -240
- package/dist/index.js.map +7 -1
- package/dist/lib/http.js +30 -39
- package/dist/lib/http.js.map +7 -1
- package/dist/lib/idempotency-store.js +34 -47
- package/dist/lib/idempotency-store.js.map +7 -1
- package/dist/lib/idempotency.js +8 -13
- package/dist/lib/idempotency.js.map +7 -1
- package/dist/lib/identifiers.js +18 -14
- package/dist/lib/identifiers.js.map +7 -1
- package/dist/lib/log.d.ts +3 -6
- package/dist/lib/log.d.ts.map +1 -1
- package/dist/lib/log.js +13 -14
- package/dist/lib/log.js.map +7 -1
- package/dist/lib/module-state.d.ts.map +1 -1
- package/dist/lib/module-state.js +32 -21
- package/dist/lib/module-state.js.map +7 -1
- package/dist/lib/order-by.d.ts +17 -0
- package/dist/lib/order-by.d.ts.map +1 -0
- package/dist/lib/order-by.js +24 -0
- package/dist/lib/order-by.js.map +7 -0
- package/dist/lib/runtime.js +16 -31
- package/dist/lib/runtime.js.map +7 -1
- package/dist/lib/sql-helpers.js +9 -27
- package/dist/lib/sql-helpers.js.map +7 -1
- package/dist/lib/sqlite.js +40 -52
- package/dist/lib/sqlite.js.map +7 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +154 -195
- package/dist/manifest-loader.js.map +7 -1
- package/dist/manifest.d.ts +3 -7
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +76 -115
- package/dist/manifest.js.map +7 -1
- package/dist/module-auth.d.ts +17 -6
- package/dist/module-auth.d.ts.map +1 -1
- package/dist/module-auth.js +93 -107
- package/dist/module-auth.js.map +7 -1
- package/dist/module-runtime-sync.d.ts +37 -0
- package/dist/module-runtime-sync.d.ts.map +1 -0
- package/dist/module-runtime-sync.js +54 -0
- package/dist/module-runtime-sync.js.map +7 -0
- package/dist/permission-gate.d.ts.map +1 -1
- package/dist/permission-gate.js +55 -86
- package/dist/permission-gate.js.map +7 -1
- package/dist/routes/_shared.d.ts +7 -2
- package/dist/routes/_shared.d.ts.map +1 -1
- package/dist/routes/_shared.js +16 -25
- package/dist/routes/_shared.js.map +7 -1
- package/dist/routes/config.d.ts +11 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +167 -213
- package/dist/routes/config.js.map +7 -1
- package/dist/routes/db-routes.d.ts +6 -10
- package/dist/routes/db-routes.d.ts.map +1 -1
- package/dist/routes/db-routes.js +198 -172
- package/dist/routes/db-routes.js.map +7 -1
- package/dist/routes/health.js +15 -20
- package/dist/routes/health.js.map +7 -1
- package/dist/routes/messages.d.ts.map +1 -1
- package/dist/routes/messages.js +56 -63
- package/dist/routes/messages.js.map +7 -1
- package/dist/routes/module-config-routes.d.ts.map +1 -1
- package/dist/routes/module-config-routes.js +92 -111
- package/dist/routes/module-config-routes.js.map +7 -1
- package/dist/routes/modules.d.ts.map +1 -1
- package/dist/routes/modules.js +82 -88
- package/dist/routes/modules.js.map +7 -1
- package/dist/routes/service-routes.d.ts.map +1 -1
- package/dist/routes/service-routes.js +54 -61
- package/dist/routes/service-routes.js.map +7 -1
- package/dist/runtime.test.js +248 -49
- package/dist/runtime.test.js.map +7 -1
- package/dist/schema-registry.d.ts.map +1 -1
- package/dist/schema-registry.js +119 -160
- package/dist/schema-registry.js.map +7 -1
- package/dist/server.js +69 -72
- package/dist/server.js.map +7 -1
- package/dist/service-registry.d.ts.map +1 -1
- package/dist/service-registry.js +60 -67
- package/dist/service-registry.js.map +7 -1
- package/dist/services/builtin-handlers.d.ts +4 -0
- package/dist/services/builtin-handlers.d.ts.map +1 -1
- package/dist/services/builtin-handlers.js +34 -20
- package/dist/services/builtin-handlers.js.map +7 -1
- package/dist/services/economy-handlers.js +143 -132
- package/dist/services/economy-handlers.js.map +7 -1
- package/dist/tx-runner.d.ts +35 -11
- package/dist/tx-runner.d.ts.map +1 -1
- package/dist/tx-runner.js +293 -250
- package/dist/tx-runner.js.map +7 -1
- package/dist/where.js +71 -94
- package/dist/where.js.map +7 -1
- package/package.json +4 -4
package/dist/manifest.js
CHANGED
|
@@ -1,128 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* manifest.ts — runtime SAPI module manifest reader.
|
|
3
|
-
*
|
|
4
|
-
* At db-server startup, scan `modules/packages/<id>/sapi/manifest.json` for
|
|
5
|
-
* every installed module and aggregate them into a single in-memory manifest.
|
|
6
|
-
* No pre-built `modules/_manifests/module-manifests.json` is required; the
|
|
7
|
-
* per-module files are the single source of truth (the SEA reads them at
|
|
8
|
-
* runtime too).
|
|
9
|
-
*
|
|
10
|
-
* Behavior:
|
|
11
|
-
* - Missing file: skip that module (WARN).
|
|
12
|
-
* - Corrupt JSON: skip + WARN.
|
|
13
|
-
* - schemaVersion > supported: WARN, continue best-effort.
|
|
14
|
-
* - schemaVersion < supported: refuse to load that module.
|
|
15
|
-
*
|
|
16
|
-
* Reconcile: every route declared in a module's manifest is checked against
|
|
17
|
-
* the prefix table; mismatches WARN but do not block startup.
|
|
18
|
-
*/
|
|
19
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
20
2
|
import { fileURLToPath } from "node:url";
|
|
21
3
|
import { dirname, resolve } from "node:path";
|
|
22
4
|
const SUPPORTED_SCHEMA = 1;
|
|
23
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* 优先级:
|
|
27
|
-
* 1. `process.env.SFMC_PACKAGES_DIR` —— sfmc/SEA 的 spawnService 在启动 db 时注入
|
|
28
|
-
* (SEA 模式下 exe 目录, npm 模式下 process.cwd())
|
|
29
|
-
* 2. `__dirname` 上溯两级到仓库根 —— 仅当 db-server 独立启动时兜底(CLI 调试)
|
|
30
|
-
*
|
|
31
|
-
* SEA 模式必须走 env 路径:SEA CJS bundle 里 `__dirname` 是虚拟路径,
|
|
32
|
-
* 不能用 `import.meta.url` 解析;靠 SFMC_PACKAGES_DIR 才是稳定的。
|
|
33
|
-
*/
|
|
34
|
-
export function defaultPackagesDir() {
|
|
35
|
-
return process.env.SFMC_PACKAGES_DIR || resolve(__dirname, "..", "..", "modules", "packages");
|
|
6
|
+
function defaultPackagesDir() {
|
|
7
|
+
return process.env.SFMC_PACKAGES_DIR || resolve(__dirname, "..", "..", "modules", "packages");
|
|
36
8
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
9
|
+
function loadManifest(packagesDir = defaultPackagesDir()) {
|
|
10
|
+
const modules = {};
|
|
11
|
+
if (!existsSync(packagesDir)) {
|
|
12
|
+
console.warn(`[manifest] ${packagesDir} does not exist; db-server will run with zero modules`);
|
|
13
|
+
return { schemaVersion: SUPPORTED_SCHEMA, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), modules };
|
|
14
|
+
}
|
|
15
|
+
const ids = readdirSync(packagesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
16
|
+
for (const id of ids) {
|
|
17
|
+
const manifestPath = resolve(packagesDir, id, "sapi", "manifest.json");
|
|
18
|
+
if (!existsSync(manifestPath)) continue;
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = readFileSync(manifestPath, "utf8");
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.warn(`[manifest] ${id}: cannot read ${manifestPath}: ${err.message}`);
|
|
24
|
+
continue;
|
|
48
25
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!existsSync(manifestPath))
|
|
56
|
-
continue;
|
|
57
|
-
let raw;
|
|
58
|
-
try {
|
|
59
|
-
raw = readFileSync(manifestPath, "utf8");
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
console.warn(`[manifest] ${id}: cannot read ${manifestPath}: ${err.message}`);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
let parsed;
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(raw);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
console.warn(`[manifest] ${id}: ${manifestPath} is not valid JSON: ${err.message}`);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > SUPPORTED_SCHEMA) {
|
|
74
|
-
console.warn(`[manifest] ${id}: schemaVersion=${parsed.schemaVersion} > supported ${SUPPORTED_SCHEMA}; parsing best-effort`);
|
|
75
|
-
}
|
|
76
|
-
modules[id] = {
|
|
77
|
-
name: parsed.name,
|
|
78
|
-
type: parsed.type,
|
|
79
|
-
configKey: parsed.configKey,
|
|
80
|
-
requires: Array.isArray(parsed.requires) ? parsed.requires : [],
|
|
81
|
-
handlers: Array.isArray(parsed.handlers) ? parsed.handlers : [],
|
|
82
|
-
routes: Array.isArray(parsed.routes) ? parsed.routes : [],
|
|
83
|
-
migrations: Array.isArray(parsed.migrations) ? parsed.migrations : [],
|
|
84
|
-
notes: parsed.notes,
|
|
85
|
-
};
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn(`[manifest] ${id}: ${manifestPath} is not valid JSON: ${err.message}`);
|
|
31
|
+
continue;
|
|
86
32
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
generatedAt: new Date().toISOString(),
|
|
90
|
-
modules,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
export function summarize(m) {
|
|
94
|
-
let routeCount = 0;
|
|
95
|
-
let handlerCount = 0;
|
|
96
|
-
let migrationCount = 0;
|
|
97
|
-
for (const entry of Object.values(m.modules)) {
|
|
98
|
-
routeCount += entry.routes.length;
|
|
99
|
-
handlerCount += entry.handlers?.length ?? 0;
|
|
100
|
-
migrationCount += entry.migrations.length;
|
|
33
|
+
if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > SUPPORTED_SCHEMA) {
|
|
34
|
+
console.warn(`[manifest] ${id}: schemaVersion=${parsed.schemaVersion} > supported ${SUPPORTED_SCHEMA}; parsing best-effort`);
|
|
101
35
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
36
|
+
modules[id] = {
|
|
37
|
+
name: parsed.name,
|
|
38
|
+
type: parsed.type,
|
|
39
|
+
configKey: parsed.configKey,
|
|
40
|
+
requires: Array.isArray(parsed.requires) ? parsed.requires : [],
|
|
41
|
+
handlers: Array.isArray(parsed.handlers) ? parsed.handlers : [],
|
|
42
|
+
routes: Array.isArray(parsed.routes) ? parsed.routes : [],
|
|
43
|
+
migrations: Array.isArray(parsed.migrations) ? parsed.migrations : [],
|
|
44
|
+
notes: parsed.notes
|
|
108
45
|
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: SUPPORTED_SCHEMA,
|
|
49
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
50
|
+
modules
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function summarize(m) {
|
|
54
|
+
let routeCount = 0;
|
|
55
|
+
let handlerCount = 0;
|
|
56
|
+
let migrationCount = 0;
|
|
57
|
+
for (const entry of Object.values(m.modules)) {
|
|
58
|
+
routeCount += entry.routes.length;
|
|
59
|
+
handlerCount += entry.handlers?.length ?? 0;
|
|
60
|
+
migrationCount += entry.migrations.length;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
moduleCount: Object.keys(m.modules).length,
|
|
64
|
+
routeCount,
|
|
65
|
+
handlerCount,
|
|
66
|
+
migrationCount,
|
|
67
|
+
moduleIds: Object.keys(m.modules).sort()
|
|
68
|
+
};
|
|
109
69
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const prefix = route.path.split("/").slice(0, 4).join("/");
|
|
120
|
-
const matched = knownRoutePrefixes.some((p) => prefix.startsWith(p.split("/").slice(0, 4).join("/")));
|
|
121
|
-
if (!matched) {
|
|
122
|
-
warnings.push(`${moduleId}: route ${route.method} ${route.path} is not covered by db-server`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
70
|
+
function reconcile(m, knownRoutePrefixes) {
|
|
71
|
+
const warnings = [];
|
|
72
|
+
for (const [moduleId, entry] of Object.entries(m.modules)) {
|
|
73
|
+
for (const route of entry.routes) {
|
|
74
|
+
const prefix = route.path.split("/").slice(0, 4).join("/");
|
|
75
|
+
const matched = knownRoutePrefixes.some((p) => prefix.startsWith(p.split("/").slice(0, 4).join("/")));
|
|
76
|
+
if (!matched) {
|
|
77
|
+
warnings.push(`${moduleId}: route ${route.method} ${route.path} is not covered by db-server`);
|
|
78
|
+
}
|
|
125
79
|
}
|
|
126
|
-
|
|
80
|
+
}
|
|
81
|
+
return warnings;
|
|
127
82
|
}
|
|
128
|
-
|
|
83
|
+
export {
|
|
84
|
+
defaultPackagesDir,
|
|
85
|
+
loadManifest,
|
|
86
|
+
reconcile,
|
|
87
|
+
summarize
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=manifest.js.map
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/manifest.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * manifest.ts \u2014 runtime SAPI module manifest reader.\n *\n * At db-server startup, scan `modules/packages/<id>/sapi/manifest.json` for\n * every installed module and aggregate them into a single in-memory manifest.\n * No pre-built `modules/_manifests/module-manifests.json` is required; the\n * per-module files are the single source of truth.\n *\n * Behavior:\n * - Missing file: skip that module (WARN).\n * - Corrupt JSON: skip + WARN.\n * - schemaVersion > supported: WARN, continue best-effort.\n * - schemaVersion < supported: refuse to load that module.\n *\n * Reconcile: every route declared in a module's manifest is checked against\n * the prefix table; mismatches WARN but do not block startup.\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\n\nexport interface ModuleManifestRoute {\n method: string;\n path: string;\n handler: string;\n}\n\nexport interface ModuleManifestMigration {\n name: string;\n version: number;\n}\n\nexport interface ModuleManifestEntry {\n name?: string | undefined;\n type?: string | undefined;\n configKey?: string | undefined;\n requires?: string[] | undefined;\n handlers?: string[] | undefined;\n routes: ModuleManifestRoute[];\n migrations: ModuleManifestMigration[];\n notes?: string | undefined;\n}\n\nexport interface ModuleManifest {\n schemaVersion: number;\n generatedAt: string;\n modules: Record<string, ModuleManifestEntry>;\n}\n\nconst SUPPORTED_SCHEMA = 1;\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport type PackagesDirResolver = () => string;\n\n/** Resolve where db-server should look for `modules/packages/`.\n *\n * \u4F18\u5148\u7EA7:\n * 1. `process.env.SFMC_PACKAGES_DIR` \u2014\u2014 sfmc spawnService \u542F\u52A8 db \u65F6\u6CE8\u5165\n * (\u901A\u5E38\u4E0E SFMC_ROOT \u5BF9\u9F50)\n * 2. `__dirname` \u4E0A\u6EAF\u4E24\u7EA7\u5230\u4ED3\u5E93\u6839 \u2014\u2014 \u4EC5\u5F53 db-server \u72EC\u7ACB\u542F\u52A8\u65F6\u515C\u5E95(CLI \u8C03\u8BD5)\n */\nexport function defaultPackagesDir(): string {\n return process.env.SFMC_PACKAGES_DIR || resolve(__dirname, \"..\", \"..\", \"modules\", \"packages\");\n}\n\n/**\n * Read every `modules/packages/<id>/sapi/manifest.json` and aggregate.\n * Modules without a manifest are silently skipped (the SAPI side will fall\n * back to a default-empty surface); modules with a corrupt or too-new\n * manifest produce warnings.\n */\nexport function loadManifest(packagesDir: string = defaultPackagesDir()): ModuleManifest {\n const modules: Record<string, ModuleManifestEntry> = {};\n if (!existsSync(packagesDir)) {\n console.warn(`[manifest] ${packagesDir} does not exist; db-server will run with zero modules`);\n return { schemaVersion: SUPPORTED_SCHEMA, generatedAt: new Date().toISOString(), modules };\n }\n const ids = readdirSync(packagesDir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .sort();\n for (const id of ids) {\n const manifestPath = resolve(packagesDir, id, \"sapi\", \"manifest.json\");\n if (!existsSync(manifestPath)) continue;\n let raw: string;\n try {\n raw = readFileSync(manifestPath, \"utf8\");\n } catch (err) {\n console.warn(`[manifest] ${id}: cannot read ${manifestPath}: ${(err as Error).message}`);\n continue;\n }\n let parsed: Partial<ModuleManifestEntry> & { schemaVersion?: number };\n try {\n parsed = JSON.parse(raw) as typeof parsed;\n } catch (err) {\n console.warn(`[manifest] ${id}: ${manifestPath} is not valid JSON: ${(err as Error).message}`);\n continue;\n }\n if (typeof parsed.schemaVersion === \"number\" && parsed.schemaVersion > SUPPORTED_SCHEMA) {\n console.warn(`[manifest] ${id}: schemaVersion=${parsed.schemaVersion} > supported ${SUPPORTED_SCHEMA}; parsing best-effort`);\n }\n modules[id] = {\n name: parsed.name,\n type: parsed.type,\n configKey: parsed.configKey,\n requires: Array.isArray(parsed.requires) ? parsed.requires : [],\n handlers: Array.isArray(parsed.handlers) ? parsed.handlers : [],\n routes: Array.isArray(parsed.routes) ? parsed.routes : [],\n migrations: Array.isArray(parsed.migrations) ? parsed.migrations : [],\n notes: parsed.notes,\n };\n }\n return {\n schemaVersion: SUPPORTED_SCHEMA,\n generatedAt: new Date().toISOString(),\n modules,\n };\n}\n\nexport interface ManifestSummary {\n moduleCount: number;\n routeCount: number;\n handlerCount: number;\n migrationCount: number;\n moduleIds: string[];\n}\n\nexport function summarize(m: ModuleManifest): ManifestSummary {\n let routeCount = 0;\n let handlerCount = 0;\n let migrationCount = 0;\n for (const entry of Object.values(m.modules)) {\n routeCount += entry.routes.length;\n handlerCount += entry.handlers?.length ?? 0;\n migrationCount += entry.migrations.length;\n }\n return {\n moduleCount: Object.keys(m.modules).length,\n routeCount,\n handlerCount,\n migrationCount,\n moduleIds: Object.keys(m.modules).sort(),\n };\n}\n\n/**\n * Reconcile manifest routes against the routes the db-server currently serves.\n * Returns a list of warnings for routes declared in the manifest that no route\n * file covers. Routes already covered by `routes/*.ts` files are not flagged.\n */\nexport function reconcile(m: ModuleManifest, knownRoutePrefixes: string[]): string[] {\n const warnings: string[] = [];\n for (const [moduleId, entry] of Object.entries(m.modules)) {\n for (const route of entry.routes) {\n const prefix = route.path.split(\"/\").slice(0, 4).join(\"/\");\n const matched = knownRoutePrefixes.some((p) => prefix.startsWith(p.split(\"/\").slice(0, 4).join(\"/\")));\n if (!matched) {\n warnings.push(`${moduleId}: route ${route.method} ${route.path} is not covered by db-server`);\n }\n }\n }\n return warnings;\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/module-auth.d.ts
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* 两套并存:
|
|
5
5
|
* - `env.AUTH_TOKEN` (config db_config.json http_auth):sfmc 管控 / 平台内部用
|
|
6
6
|
* - `moduleTokens[moduleId]`:db-server 给每个 enabled 模块在启动时派生,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 写到「与数据库同目录」的 module-tokens.json(跟 db_config.dbDir 走,勿写死 ROOT/data),
|
|
8
|
+
* SAPI 经 loopback API 注入 setDbModuleContext。
|
|
9
9
|
*
|
|
10
10
|
* 派生公式:
|
|
11
11
|
* module_token = HMAC-SHA256(auth_secret, "sfmc-module:" + moduleId).hex
|
|
12
12
|
*
|
|
13
13
|
* auth_secret 优先级:
|
|
14
14
|
* 1. env.AUTH_TOKEN
|
|
15
|
-
* 2. 启动时随机生成(无 AUTH_TOKEN 时),写到
|
|
15
|
+
* 2. 启动时随机生成(无 AUTH_TOKEN 时),写到 module-tokens.json,
|
|
16
16
|
* 只警告一次(每次重启模块 token 都变,SAPI host 必须重读文件)
|
|
17
17
|
*
|
|
18
18
|
* 校验:
|
|
@@ -27,20 +27,31 @@ export interface ModuleAuthMap {
|
|
|
27
27
|
/** 用于派生 token 的 secret (env.AUTH_TOKEN 或本次启动随机生成) */
|
|
28
28
|
secret: string;
|
|
29
29
|
}
|
|
30
|
+
/** token 文件与 SQLite 同目录(由 dbDir 决定,避免向导选目录前就创建 ROOT/data) */
|
|
31
|
+
export declare function moduleTokensFile(dbPath: string): string;
|
|
30
32
|
export declare function deriveToken(moduleId: string, secret: string): string;
|
|
31
33
|
/**
|
|
32
34
|
* 启动时调用一次:
|
|
33
35
|
* - 把 env.AUTH_TOKEN 当 secret
|
|
34
36
|
* - 给每个 enabled moduleId 派生 token
|
|
35
|
-
* - 写
|
|
37
|
+
* - 写 module-tokens.json(与 DB 同目录)
|
|
36
38
|
* - 控制台打印摘要(env var SFMC_LOG_MODULE_TOKEN=1 时打完整)
|
|
37
39
|
*/
|
|
38
40
|
export declare function buildModuleAuth(opts: {
|
|
39
|
-
|
|
41
|
+
dbPath: string;
|
|
40
42
|
envAuthToken: string;
|
|
41
43
|
enabledModuleIds: string[];
|
|
42
44
|
}): ModuleAuthMap;
|
|
43
|
-
export declare function loadModuleAuth(
|
|
45
|
+
export declare function loadModuleAuth(dbPath: string): ModuleAuthMap | null;
|
|
46
|
+
/** 把当前 auth map 写回 module-tokens.json(热更新 token 时复用) */
|
|
47
|
+
export declare function persistModuleAuth(dbPath: string, auth: ModuleAuthMap, envAuthToken: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* 确保 moduleId 在 auth.tokens 中有派生值(复用现有 secret,勿重建随机 secret)。
|
|
50
|
+
* @returns 是否新写入了 token
|
|
51
|
+
*/
|
|
52
|
+
export declare function ensureModuleToken(auth: ModuleAuthMap, moduleId: string): boolean;
|
|
53
|
+
/** 禁用时撤销内存中的 token(磁盘由 persistModuleAuth 同步) */
|
|
54
|
+
export declare function revokeModuleToken(auth: ModuleAuthMap, moduleId: string): boolean;
|
|
44
55
|
/**
|
|
45
56
|
* 校验请求方身份:
|
|
46
57
|
* - Bearer 头 / X-Module-Token 二选一
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-auth.d.ts","sourceRoot":"","sources":["../src/module-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AASD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,
|
|
1
|
+
{"version":3,"file":"module-auth.d.ts","sourceRoot":"","sources":["../src/module-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AASD,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B,GAAG,aAAa,CAqChB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAInE;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAQjG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIhF;AAED,gDAAgD;AAChD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIhF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IACxC,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC/B,GAAG,MAAM,GAAG,IAAI,CAuBhB"}
|
package/dist/module-auth.js
CHANGED
|
@@ -1,118 +1,104 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* module-auth.ts — 模块身份的 HMAC token 派生与校验
|
|
3
|
-
*
|
|
4
|
-
* 两套并存:
|
|
5
|
-
* - `env.AUTH_TOKEN` (config db_config.json http_auth):sfmc 管控 / 平台内部用
|
|
6
|
-
* - `moduleTokens[moduleId]`:db-server 给每个 enabled 模块在启动时派生,
|
|
7
|
-
* 写到 data/module-tokens.json,SAPI host-bootstrap 启动时读,按
|
|
8
|
-
* `setDbModuleContext(moduleId, token)` 注入。
|
|
9
|
-
*
|
|
10
|
-
* 派生公式:
|
|
11
|
-
* module_token = HMAC-SHA256(auth_secret, "sfmc-module:" + moduleId).hex
|
|
12
|
-
*
|
|
13
|
-
* auth_secret 优先级:
|
|
14
|
-
* 1. env.AUTH_TOKEN
|
|
15
|
-
* 2. 启动时随机生成(无 AUTH_TOKEN 时),写到 data/module-tokens.json,
|
|
16
|
-
* 只警告一次(每次重启模块 token 都变,SAPI host 必须重读文件)
|
|
17
|
-
*
|
|
18
|
-
* 校验:
|
|
19
|
-
* - HTTP `Authorization: Bearer <module_token>` 头
|
|
20
|
-
* - Query `?moduleId=<id>`
|
|
21
|
-
* - 两都匹配 = 调用方身份
|
|
22
|
-
* - 校验调用方 moduleId 在 enabled 列表(否则 403)
|
|
23
|
-
*/
|
|
24
1
|
import { readJson, writeJson } from "@sfmc-bds/sdk/node/config";
|
|
25
2
|
import { createHmac, randomBytes } from "node:crypto";
|
|
26
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
27
4
|
import { log } from "./lib/log.js";
|
|
28
5
|
const IDENT = /^[A-Za-z0-9_\-]+$/;
|
|
29
6
|
function safeId(id) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return id;
|
|
7
|
+
if (!IDENT.test(id)) throw new Error(`[auth] invalid module id: ${id}`);
|
|
8
|
+
return id;
|
|
33
9
|
}
|
|
34
|
-
|
|
35
|
-
|
|
10
|
+
function moduleTokensFile(dbPath) {
|
|
11
|
+
return join(dirname(dbPath), "module-tokens.json");
|
|
36
12
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (const [id, t] of Object.entries(tokens)) {
|
|
66
|
-
console.log(` ${id} = ${t}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
const lines = Object.entries(tokens)
|
|
71
|
-
.map(([id, t]) => ` ${id} = ${t.slice(0, 6)}…${t.slice(-4)}`)
|
|
72
|
-
.join("\n");
|
|
73
|
-
log.info(`[auth] token 摘要:\n${lines}`);
|
|
13
|
+
function deriveToken(moduleId, secret) {
|
|
14
|
+
return createHmac("sha256", secret).update(`sfmc-module:${safeId(moduleId)}`).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function buildModuleAuth(opts) {
|
|
17
|
+
const secret = opts.envAuthToken || randomBytes(32).toString("hex");
|
|
18
|
+
const tokens = {};
|
|
19
|
+
for (const id of opts.enabledModuleIds) {
|
|
20
|
+
tokens[id] = deriveToken(id, secret);
|
|
21
|
+
}
|
|
22
|
+
const outFile = moduleTokensFile(opts.dbPath);
|
|
23
|
+
writeJson(outFile, {
|
|
24
|
+
tokens,
|
|
25
|
+
secret,
|
|
26
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27
|
+
secretGenerated: !opts.envAuthToken
|
|
28
|
+
});
|
|
29
|
+
log.success(
|
|
30
|
+
`[auth] \u5DF2\u4E3A ${opts.enabledModuleIds.length} \u4E2A\u6A21\u5757\u6D3E\u751F HMAC token,\u5199\u5165 ${outFile}`
|
|
31
|
+
);
|
|
32
|
+
if (!opts.envAuthToken) {
|
|
33
|
+
log.warn(
|
|
34
|
+
"[auth] \u672A\u914D\u7F6E env.AUTH_TOKEN(\u51B2\u7A81 config http_auth),\u672C\u6B21\u542F\u52A8\u4F7F\u7528\u4E86\u968F\u673A secret; \u91CD\u542F\u540E token \u4F1A\u53D8,host-bootstrap \u5FC5\u987B\u6BCF\u6B21\u91CD\u65B0\u8BFB module-tokens.json"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (process.env.SFMC_LOG_MODULE_TOKEN === "1") {
|
|
38
|
+
log.info("[auth] module tokens (SFMC_LOG_MODULE_TOKEN=1):");
|
|
39
|
+
for (const [id, t] of Object.entries(tokens)) {
|
|
40
|
+
console.log(` ${id} = ${t}`);
|
|
74
41
|
}
|
|
75
|
-
|
|
42
|
+
} else {
|
|
43
|
+
const lines = Object.entries(tokens).map(([id, t]) => ` ${id} = ${t.slice(0, 6)}\u2026${t.slice(-4)}`).join("\n");
|
|
44
|
+
log.info(`[auth] token \u6458\u8981:
|
|
45
|
+
${lines}`);
|
|
46
|
+
}
|
|
47
|
+
return { tokens, secret };
|
|
76
48
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return { tokens: parsed.tokens, secret: parsed.secret };
|
|
49
|
+
function loadModuleAuth(dbPath) {
|
|
50
|
+
const parsed = readJson(moduleTokensFile(dbPath));
|
|
51
|
+
if (!parsed || !parsed.tokens || !parsed.secret) return null;
|
|
52
|
+
return { tokens: parsed.tokens, secret: parsed.secret };
|
|
82
53
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
54
|
+
function persistModuleAuth(dbPath, auth, envAuthToken) {
|
|
55
|
+
const outFile = moduleTokensFile(dbPath);
|
|
56
|
+
writeJson(outFile, {
|
|
57
|
+
tokens: auth.tokens,
|
|
58
|
+
secret: auth.secret,
|
|
59
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
|
+
secretGenerated: !envAuthToken
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function ensureModuleToken(auth, moduleId) {
|
|
64
|
+
if (auth.tokens[moduleId]) return false;
|
|
65
|
+
auth.tokens[moduleId] = deriveToken(moduleId, auth.secret);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function revokeModuleToken(auth, moduleId) {
|
|
69
|
+
if (!(moduleId in auth.tokens)) return false;
|
|
70
|
+
delete auth.tokens[moduleId];
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function verifyModuleAuth(opts) {
|
|
74
|
+
const moduleId = opts.params.get("moduleId") || "";
|
|
75
|
+
if (!moduleId || !opts.enabledModuleIds.has(moduleId)) return null;
|
|
76
|
+
let provided = "";
|
|
77
|
+
const auth = opts.headers["authorization"];
|
|
78
|
+
const token = opts.headers["x-module-token"];
|
|
79
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
80
|
+
provided = auth.slice(7);
|
|
81
|
+
} else if (typeof token === "string") {
|
|
82
|
+
provided = token;
|
|
83
|
+
}
|
|
84
|
+
if (!provided) return null;
|
|
85
|
+
const expected = opts.auth.tokens[moduleId];
|
|
86
|
+
if (!expected) return null;
|
|
87
|
+
if (provided.length !== expected.length) return null;
|
|
88
|
+
let diff = 0;
|
|
89
|
+
for (let i = 0; i < provided.length; i++) {
|
|
90
|
+
diff |= provided.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
91
|
+
}
|
|
92
|
+
return diff === 0 ? moduleId : null;
|
|
117
93
|
}
|
|
118
|
-
|
|
94
|
+
export {
|
|
95
|
+
buildModuleAuth,
|
|
96
|
+
deriveToken,
|
|
97
|
+
ensureModuleToken,
|
|
98
|
+
loadModuleAuth,
|
|
99
|
+
moduleTokensFile,
|
|
100
|
+
persistModuleAuth,
|
|
101
|
+
revokeModuleToken,
|
|
102
|
+
verifyModuleAuth
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=module-auth.js.map
|
package/dist/module-auth.js.map
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/module-auth.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * module-auth.ts \u2014 \u6A21\u5757\u8EAB\u4EFD\u7684 HMAC token \u6D3E\u751F\u4E0E\u6821\u9A8C\n *\n * \u4E24\u5957\u5E76\u5B58:\n * - `env.AUTH_TOKEN` (config db_config.json http_auth):sfmc \u7BA1\u63A7 / \u5E73\u53F0\u5185\u90E8\u7528\n * - `moduleTokens[moduleId]`:db-server \u7ED9\u6BCF\u4E2A enabled \u6A21\u5757\u5728\u542F\u52A8\u65F6\u6D3E\u751F,\n * \u5199\u5230\u300C\u4E0E\u6570\u636E\u5E93\u540C\u76EE\u5F55\u300D\u7684 module-tokens.json\uFF08\u8DDF db_config.dbDir \u8D70\uFF0C\u52FF\u5199\u6B7B ROOT/data\uFF09,\n * SAPI \u7ECF loopback API \u6CE8\u5165 setDbModuleContext\u3002\n *\n * \u6D3E\u751F\u516C\u5F0F:\n * module_token = HMAC-SHA256(auth_secret, \"sfmc-module:\" + moduleId).hex\n *\n * auth_secret \u4F18\u5148\u7EA7:\n * 1. env.AUTH_TOKEN\n * 2. \u542F\u52A8\u65F6\u968F\u673A\u751F\u6210(\u65E0 AUTH_TOKEN \u65F6),\u5199\u5230 module-tokens.json,\n * \u53EA\u8B66\u544A\u4E00\u6B21(\u6BCF\u6B21\u91CD\u542F\u6A21\u5757 token \u90FD\u53D8,SAPI host \u5FC5\u987B\u91CD\u8BFB\u6587\u4EF6)\n *\n * \u6821\u9A8C:\n * - HTTP `Authorization: Bearer <module_token>` \u5934\n * - Query `?moduleId=<id>`\n * - \u4E24\u90FD\u5339\u914D = \u8C03\u7528\u65B9\u8EAB\u4EFD\n * - \u6821\u9A8C\u8C03\u7528\u65B9 moduleId \u5728 enabled \u5217\u8868(\u5426\u5219 403)\n */\n\nimport { readJson, writeJson, type TokenStore } from \"@sfmc-bds/sdk/node/config\";\nimport { createHmac, randomBytes } from \"node:crypto\";\nimport { dirname, join } from \"node:path\";\nimport { log } from \"./lib/log.js\";\n\nexport interface ModuleAuthMap {\n /** moduleId \u2192 hex token */\n tokens: Record<string, string>;\n /** \u7528\u4E8E\u6D3E\u751F token \u7684 secret (env.AUTH_TOKEN \u6216\u672C\u6B21\u542F\u52A8\u968F\u673A\u751F\u6210) */\n secret: string;\n}\n\nconst IDENT = /^[A-Za-z0-9_\\-]+$/;\n\nfunction safeId(id: string): string {\n if (!IDENT.test(id)) throw new Error(`[auth] invalid module id: ${id}`);\n return id;\n}\n\n/** token \u6587\u4EF6\u4E0E SQLite \u540C\u76EE\u5F55\uFF08\u7531 dbDir \u51B3\u5B9A\uFF0C\u907F\u514D\u5411\u5BFC\u9009\u76EE\u5F55\u524D\u5C31\u521B\u5EFA ROOT/data\uFF09 */\nexport function moduleTokensFile(dbPath: string): string {\n return join(dirname(dbPath), \"module-tokens.json\");\n}\n\nexport function deriveToken(moduleId: string, secret: string): string {\n return createHmac(\"sha256\", secret).update(`sfmc-module:${safeId(moduleId)}`).digest(\"hex\");\n}\n\n/**\n * \u542F\u52A8\u65F6\u8C03\u7528\u4E00\u6B21:\n * - \u628A env.AUTH_TOKEN \u5F53 secret\n * - \u7ED9\u6BCF\u4E2A enabled moduleId \u6D3E\u751F token\n * - \u5199 module-tokens.json\uFF08\u4E0E DB \u540C\u76EE\u5F55\uFF09\n * - \u63A7\u5236\u53F0\u6253\u5370\u6458\u8981(env var SFMC_LOG_MODULE_TOKEN=1 \u65F6\u6253\u5B8C\u6574)\n */\nexport function buildModuleAuth(opts: {\n dbPath: string;\n envAuthToken: string;\n enabledModuleIds: string[];\n}): ModuleAuthMap {\n const secret = opts.envAuthToken || randomBytes(32).toString(\"hex\");\n const tokens: Record<string, string> = {};\n for (const id of opts.enabledModuleIds) {\n tokens[id] = deriveToken(id, secret);\n }\n\n const outFile = moduleTokensFile(opts.dbPath);\n writeJson(outFile, {\n tokens,\n secret,\n generatedAt: new Date().toISOString(),\n secretGenerated: !opts.envAuthToken,\n });\n\n log.success(\n `[auth] \u5DF2\u4E3A ${opts.enabledModuleIds.length} \u4E2A\u6A21\u5757\u6D3E\u751F HMAC token,\u5199\u5165 ${outFile}`\n );\n if (!opts.envAuthToken) {\n log.warn(\n \"[auth] \u672A\u914D\u7F6E env.AUTH_TOKEN(\u51B2\u7A81 config http_auth),\u672C\u6B21\u542F\u52A8\u4F7F\u7528\u4E86\u968F\u673A secret;\"\n + \" \u91CD\u542F\u540E token \u4F1A\u53D8,host-bootstrap \u5FC5\u987B\u6BCF\u6B21\u91CD\u65B0\u8BFB module-tokens.json\"\n );\n }\n if (process.env.SFMC_LOG_MODULE_TOKEN === \"1\") {\n log.info(\"[auth] module tokens (SFMC_LOG_MODULE_TOKEN=1):\");\n for (const [id, t] of Object.entries(tokens)) {\n console.log(` ${id} = ${t}`);\n }\n } else {\n const lines = Object.entries(tokens)\n .map(([id, t]) => ` ${id} = ${t.slice(0, 6)}\u2026${t.slice(-4)}`)\n .join(\"\\n\");\n log.info(`[auth] token \u6458\u8981:\\n${lines}`);\n }\n\n return { tokens, secret };\n}\n\nexport function loadModuleAuth(dbPath: string): ModuleAuthMap | null {\n const parsed = readJson<TokenStore>(moduleTokensFile(dbPath));\n if (!parsed || !parsed.tokens || !parsed.secret) return null;\n return { tokens: parsed.tokens, secret: parsed.secret };\n}\n\n/** \u628A\u5F53\u524D auth map \u5199\u56DE module-tokens.json(\u70ED\u66F4\u65B0 token \u65F6\u590D\u7528) */\nexport function persistModuleAuth(dbPath: string, auth: ModuleAuthMap, envAuthToken: string): void {\n const outFile = moduleTokensFile(dbPath);\n writeJson(outFile, {\n tokens: auth.tokens,\n secret: auth.secret,\n generatedAt: new Date().toISOString(),\n secretGenerated: !envAuthToken,\n } satisfies TokenStore);\n}\n\n/**\n * \u786E\u4FDD moduleId \u5728 auth.tokens \u4E2D\u6709\u6D3E\u751F\u503C(\u590D\u7528\u73B0\u6709 secret,\u52FF\u91CD\u5EFA\u968F\u673A secret)\u3002\n * @returns \u662F\u5426\u65B0\u5199\u5165\u4E86 token\n */\nexport function ensureModuleToken(auth: ModuleAuthMap, moduleId: string): boolean {\n if (auth.tokens[moduleId]) return false;\n auth.tokens[moduleId] = deriveToken(moduleId, auth.secret);\n return true;\n}\n\n/** \u7981\u7528\u65F6\u64A4\u9500\u5185\u5B58\u4E2D\u7684 token(\u78C1\u76D8\u7531 persistModuleAuth \u540C\u6B65) */\nexport function revokeModuleToken(auth: ModuleAuthMap, moduleId: string): boolean {\n if (!(moduleId in auth.tokens)) return false;\n delete auth.tokens[moduleId];\n return true;\n}\n\n/**\n * \u6821\u9A8C\u8BF7\u6C42\u65B9\u8EAB\u4EFD:\n * - Bearer \u5934 / X-Module-Token \u4E8C\u9009\u4E00\n * - ?moduleId=<id> \u5FC5\u987B\u5B58\u5728\n * - \u6A21\u5757\u5FC5\u987B\u5728 enabledModuleIds \u96C6\u5408\u4E2D\n * - token \u5FC5\u987B = deriveToken(moduleId, secret)\n * \u8FD4\u56DE\u6A21\u5757 ID \u6216 null\u3002\n */\nexport function verifyModuleAuth(opts: {\n headers: NodeJS.Dict<string | string[]>;\n params: URLSearchParams;\n auth: ModuleAuthMap;\n enabledModuleIds: Set<string>;\n}): string | null {\n const moduleId = opts.params.get(\"moduleId\") || \"\";\n if (!moduleId || !opts.enabledModuleIds.has(moduleId)) return null;\n\n let provided = \"\";\n const auth = opts.headers[\"authorization\"];\n const token = opts.headers[\"x-module-token\"];\n if (typeof auth === \"string\" && auth.startsWith(\"Bearer \")) {\n provided = auth.slice(7);\n } else if (typeof token === \"string\") {\n provided = token;\n }\n if (!provided) return null;\n\n const expected = opts.auth.tokens[moduleId];\n if (!expected) return null;\n // constant-time \u6BD4\u8F83\n if (provided.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < provided.length; i++) {\n diff |= provided.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return diff === 0 ? moduleId : null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* module-runtime-sync.ts — enable/disable 时同步进程内运行态
|
|
3
|
+
*
|
|
4
|
+
* lock 落盘后,内存态(enabledSet / enabledManifests / moduleAuth.tokens /
|
|
5
|
+
* builtin service handlers)必须与之一致,否则「只重启 BDS」拿不到新 token
|
|
6
|
+
* (DIP:路由只依赖注入的集合,不感知启停实现)。
|
|
7
|
+
*
|
|
8
|
+
* DRY:token 落盘 / 内置 handler 启停复用 module-auth 与 builtin-handlers,
|
|
9
|
+
* 不在此处再写一份派生与卸载逻辑。
|
|
10
|
+
*/
|
|
11
|
+
import type { ModuleManifestV2 } from "./manifest-loader.js";
|
|
12
|
+
import { type ModuleAuthMap } from "./module-auth.js";
|
|
13
|
+
import { type BuiltinServiceDeps } from "./services/builtin-handlers.js";
|
|
14
|
+
import type { ServiceRegistry } from "./service-registry.js";
|
|
15
|
+
export type LoadedModules = {
|
|
16
|
+
modules: Record<string, ModuleManifestV2 | undefined>;
|
|
17
|
+
};
|
|
18
|
+
export type SyncModuleRuntimeOpts = {
|
|
19
|
+
moduleId: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** SQLite 路径;token 文件写在同目录 */
|
|
22
|
+
dbPath: string;
|
|
23
|
+
/** 与 buildModuleAuth 一致:无 AUTH_TOKEN 时 secretGenerated=true */
|
|
24
|
+
envAuthToken: string;
|
|
25
|
+
enabledSet: Set<string>;
|
|
26
|
+
enabledManifests: Map<string, ModuleManifestV2>;
|
|
27
|
+
loadedManifest: LoadedModules;
|
|
28
|
+
moduleAuth: ModuleAuthMap;
|
|
29
|
+
serviceRegistry: ServiceRegistry;
|
|
30
|
+
builtinDeps: BuiltinServiceDeps;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 同步 enable/disable 后的进程内集合与 token 文件。
|
|
34
|
+
* 调用方负责先写 lock;本函数不碰磁盘 lock。
|
|
35
|
+
*/
|
|
36
|
+
export declare function syncModuleRuntimeState(opts: SyncModuleRuntimeOpts): void;
|
|
37
|
+
//# sourceMappingURL=module-runtime-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-runtime-sync.d.ts","sourceRoot":"","sources":["../src/module-runtime-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,cAAc,EAAE,aAAa,CAAC;IAC9B,UAAU,EAAE,aAAa,CAAC;IAC1B,eAAe,EAAE,eAAe,CAAC;IACjC,WAAW,EAAE,kBAAkB,CAAC;CACjC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAyCxE"}
|