@zlooks.cn/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/command/index.d.ts +3 -0
  2. package/dist/command/index.d.ts.map +1 -0
  3. package/dist/command/index.js +145 -0
  4. package/dist/command/index.js.map +1 -0
  5. package/dist/deploy.d.ts +35 -0
  6. package/dist/deploy.d.ts.map +1 -0
  7. package/dist/deploy.js +312 -0
  8. package/dist/deploy.js.map +1 -0
  9. package/dist/deployment.d.ts +18 -0
  10. package/dist/deployment.d.ts.map +1 -0
  11. package/dist/deployment.js +26 -0
  12. package/dist/deployment.js.map +1 -0
  13. package/dist/doctor.d.ts +24 -0
  14. package/dist/doctor.d.ts.map +1 -0
  15. package/dist/doctor.js +78 -0
  16. package/dist/doctor.js.map +1 -0
  17. package/dist/export/export-operation.d.ts +34 -0
  18. package/dist/export/export-operation.d.ts.map +1 -0
  19. package/dist/export/export-operation.js +69 -0
  20. package/dist/export/export-operation.js.map +1 -0
  21. package/dist/export/legacy-export.d.ts +129 -0
  22. package/dist/export/legacy-export.d.ts.map +1 -0
  23. package/dist/export/legacy-export.js +334 -0
  24. package/dist/export/legacy-export.js.map +1 -0
  25. package/dist/export/legacy-source.d.ts +16 -0
  26. package/dist/export/legacy-source.d.ts.map +1 -0
  27. package/dist/export/legacy-source.js +164 -0
  28. package/dist/export/legacy-source.js.map +1 -0
  29. package/dist/global-packages.d.ts +24 -0
  30. package/dist/global-packages.d.ts.map +1 -0
  31. package/dist/global-packages.js +89 -0
  32. package/dist/global-packages.js.map +1 -0
  33. package/dist/import/import-operation.d.ts +6 -0
  34. package/dist/import/import-operation.d.ts.map +1 -0
  35. package/dist/import/import-operation.js +16 -0
  36. package/dist/import/import-operation.js.map +1 -0
  37. package/dist/npm-registry.d.ts +11 -0
  38. package/dist/npm-registry.d.ts.map +1 -0
  39. package/dist/npm-registry.js +20 -0
  40. package/dist/npm-registry.js.map +1 -0
  41. package/dist/operations.d.ts +33 -0
  42. package/dist/operations.d.ts.map +1 -0
  43. package/dist/operations.js +270 -0
  44. package/dist/operations.js.map +1 -0
  45. package/dist/package-manifest.d.ts +16 -0
  46. package/dist/package-manifest.d.ts.map +1 -0
  47. package/dist/package-manifest.js +72 -0
  48. package/dist/package-manifest.js.map +1 -0
  49. package/dist/package-updates.d.ts +13 -0
  50. package/dist/package-updates.d.ts.map +1 -0
  51. package/dist/package-updates.js +41 -0
  52. package/dist/package-updates.js.map +1 -0
  53. package/dist/pm2.d.ts +28 -0
  54. package/dist/pm2.d.ts.map +1 -0
  55. package/dist/pm2.js +72 -0
  56. package/dist/pm2.js.map +1 -0
  57. package/dist/prompt.d.ts +49 -0
  58. package/dist/prompt.d.ts.map +1 -0
  59. package/dist/prompt.js +25 -0
  60. package/dist/prompt.js.map +1 -0
  61. package/dist/release-environment.d.ts +6 -0
  62. package/dist/release-environment.d.ts.map +1 -0
  63. package/dist/release-environment.js +19 -0
  64. package/dist/release-environment.js.map +1 -0
  65. package/dist/service-catalog.d.ts +10 -0
  66. package/dist/service-catalog.d.ts.map +1 -0
  67. package/dist/service-catalog.js +28 -0
  68. package/dist/service-catalog.js.map +1 -0
  69. package/dist/setup.d.ts +39 -0
  70. package/dist/setup.d.ts.map +1 -0
  71. package/dist/setup.js +413 -0
  72. package/dist/setup.js.map +1 -0
  73. package/dist/system.d.ts +5 -0
  74. package/dist/system.d.ts.map +1 -0
  75. package/dist/system.js +11 -0
  76. package/dist/system.js.map +1 -0
  77. package/dist/topology.d.ts +11 -0
  78. package/dist/topology.d.ts.map +1 -0
  79. package/dist/topology.js +78 -0
  80. package/dist/topology.js.map +1 -0
  81. package/package.json +51 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,78 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { createConnection } from 'node:net';
3
+ import { resolve } from 'node:path';
4
+ import { parseEnv } from 'node:util';
5
+ import { parseRegistryBootstrap } from '@zlooks.cn/registry-config';
6
+ import { selectServices } from './service-catalog.js';
7
+ export async function inspectDeployment(options, runtime = defaultRuntime) {
8
+ const services = selectServices(options.serviceIds);
9
+ const checks = [];
10
+ const nodeMajor = Number(runtime.nodeVersion.split('.')[0]);
11
+ checks.push(check('node', nodeMajor >= 24, `Node ${runtime.nodeVersion}; required >=24`));
12
+ const envExists = runtime.exists(options.envFile);
13
+ checks.push(check('env-file', envExists, options.envFile));
14
+ let registry;
15
+ if (envExists) {
16
+ try {
17
+ const env = parseEnv(runtime.readText(options.envFile));
18
+ registry = parseRegistryBootstrap({
19
+ REGISTRY_HOST: env.REGISTRY_HOST,
20
+ REGISTRY_PORT: env.REGISTRY_PORT,
21
+ });
22
+ }
23
+ catch {
24
+ registry = undefined;
25
+ }
26
+ }
27
+ checks.push(check('registry-config', registry !== undefined, registry ? `${registry.host}:${registry.port}` : 'REGISTRY_HOST/REGISTRY_PORT are invalid'));
28
+ const registryReachable = registry
29
+ ? await runtime.canConnect(registry.host, registry.port)
30
+ : false;
31
+ checks.push(check('registry-connectivity', registryReachable, registry ? `${registry.host}:${registry.port}` : 'Registry bootstrap is unavailable'));
32
+ for (const service of services) {
33
+ const packageRoot = resolve(options.workspaceRoot, 'packages', service.id);
34
+ const bootFiles = runtime.findBootFiles(resolve(packageRoot, 'dist'));
35
+ checks.push(check(`${service.id}:boot`, bootFiles.length > 0 && bootFiles.every(runtime.exists), bootFiles.length > 0 ? bootFiles.join(', ') : 'No dist/**/*.boot.js file found'));
36
+ if (service.requiresRscArtifact) {
37
+ const artifactRoot = resolve(packageRoot, '.hile-rsc');
38
+ checks.push(check(`${service.id}:rsc-artifact`, runtime.exists(artifactRoot), artifactRoot));
39
+ }
40
+ }
41
+ return { ok: checks.every((current) => current.ok), checks };
42
+ }
43
+ function check(id, ok, detail) {
44
+ return { id, ok, detail };
45
+ }
46
+ function findBootFiles(directory) {
47
+ if (!existsSync(directory))
48
+ return [];
49
+ const result = [];
50
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
51
+ const path = resolve(directory, entry.name);
52
+ if (entry.isDirectory())
53
+ result.push(...findBootFiles(path));
54
+ if (entry.isFile() && entry.name.endsWith('.boot.js'))
55
+ result.push(path);
56
+ }
57
+ return result;
58
+ }
59
+ async function canConnect(host, port) {
60
+ return new Promise((complete) => {
61
+ const socket = createConnection({ host, port });
62
+ const finish = (result) => {
63
+ socket.destroy();
64
+ complete(result);
65
+ };
66
+ socket.setTimeout(1_500, () => finish(false));
67
+ socket.once('connect', () => finish(true));
68
+ socket.once('error', () => finish(false));
69
+ });
70
+ }
71
+ const defaultRuntime = {
72
+ canConnect,
73
+ exists: existsSync,
74
+ findBootFiles,
75
+ nodeVersion: process.versions.node,
76
+ readText: (path) => readFileSync(path, 'utf8'),
77
+ };
78
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type LegacyExportSnapshot } from './legacy-export.js';
2
+ export interface LegacySourceConfiguration {
3
+ readonly postgres: {
4
+ readonly host: string;
5
+ readonly port: number;
6
+ readonly username: string;
7
+ readonly password: string;
8
+ readonly database: string;
9
+ };
10
+ readonly redis: {
11
+ readonly host: string;
12
+ readonly port: number;
13
+ readonly db: number;
14
+ readonly username?: string;
15
+ readonly password?: string;
16
+ };
17
+ readonly redisConfigsKey: string;
18
+ }
19
+ export interface LegacyExportSource {
20
+ read(): Promise<LegacyExportSnapshot>;
21
+ close(): Promise<void>;
22
+ }
23
+ export interface ExportOperationOptions {
24
+ readonly sourceEnvironmentFile: string;
25
+ readonly homeDirectory?: string;
26
+ readonly sourceFactory?: (config: LegacySourceConfiguration) => Promise<LegacyExportSource>;
27
+ readonly clock?: () => Date;
28
+ readonly write?: (text: string) => void;
29
+ }
30
+ export declare function exportOperation(options: ExportOperationOptions): Promise<{
31
+ readonly outputDirectory: string;
32
+ }>;
33
+ export declare function parseLegacySourceEnvironment(source: string): LegacySourceConfiguration;
34
+ //# sourceMappingURL=export-operation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-operation.d.ts","sourceRoot":"","sources":["../../src/export/export-operation.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,oBAAoB,CAAC;AAG5B,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,yBAAyB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC5F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC9E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,CAAC,CAcD;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,yBAAyB,CA4BtF"}
@@ -0,0 +1,69 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { resolve } from 'node:path';
4
+ import { parseEnv } from 'node:util';
5
+ import { buildLegacyExport, writeLegacyExport, } from './legacy-export.js';
6
+ import { createTypeOrmLegacyExportSource } from './legacy-source.js';
7
+ export async function exportOperation(options) {
8
+ const environmentFile = resolve(process.cwd(), options.sourceEnvironmentFile);
9
+ const config = parseLegacySourceEnvironment(await readFile(environmentFile, 'utf8'));
10
+ const sourceFactory = options.sourceFactory ?? createTypeOrmLegacyExportSource;
11
+ const source = await sourceFactory(config);
12
+ const outputDirectory = resolve(options.homeDirectory ?? homedir(), '.zlooks.cn/export');
13
+ try {
14
+ const document = buildLegacyExport(await source.read());
15
+ await writeLegacyExport(outputDirectory, document, (options.clock ?? (() => new Date()))());
16
+ }
17
+ finally {
18
+ await source.close();
19
+ }
20
+ (options.write ?? ((text) => process.stdout.write(text)))(`导出完成:${outputDirectory}\n`);
21
+ return { outputDirectory };
22
+ }
23
+ export function parseLegacySourceEnvironment(source) {
24
+ const environment = parseEnv(source);
25
+ if (environment.TYPEORM_TYPE?.trim().toLocaleLowerCase('en-US') !== 'postgres') {
26
+ throw new Error('zlooks export 仅支持从 PostgreSQL 导出');
27
+ }
28
+ const username = required(environment.TYPEORM_USERNAME, 'TYPEORM_USERNAME');
29
+ const password = environment.TYPEORM_PASSWORD ?? '';
30
+ const redisUsername = optional(environment.REDIS_USERNAME);
31
+ const redisPassword = optional(environment.REDIS_PASSWORD);
32
+ return {
33
+ postgres: {
34
+ host: required(environment.TYPEORM_HOST, 'TYPEORM_HOST'),
35
+ port: integer(environment.TYPEORM_PORT, 'TYPEORM_PORT', 1, 65_535),
36
+ username,
37
+ password,
38
+ database: required(environment.TYPEORM_DATABASE, 'TYPEORM_DATABASE'),
39
+ },
40
+ redis: {
41
+ host: required(environment.REDIS_HOST, 'REDIS_HOST'),
42
+ port: integer(environment.REDIS_PORT, 'REDIS_PORT', 1, 65_535),
43
+ db: integer(environment.REDIS_DB ?? '0', 'REDIS_DB', 0, 15),
44
+ ...(redisUsername ? { username: redisUsername } : {}),
45
+ ...(redisPassword ? { password: redisPassword } : {}),
46
+ },
47
+ redisConfigsKey: optional(environment.REDIS_CONFIGS_KEY)
48
+ ?? optional(environment.REDIS_CONFIGS_KEY_PREFIX)
49
+ ?? 'www:site:configs',
50
+ };
51
+ }
52
+ function required(value, name) {
53
+ const result = optional(value);
54
+ if (!result)
55
+ throw new Error(`旧项目环境变量 ${name} 不能为空`);
56
+ return result;
57
+ }
58
+ function optional(value) {
59
+ const result = value?.trim();
60
+ return result || undefined;
61
+ }
62
+ function integer(value, name, minimum, maximum) {
63
+ const parsed = Number(value);
64
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
65
+ throw new Error(`旧项目环境变量 ${name} 必须是 ${minimum}..${maximum} 的整数`);
66
+ }
67
+ return parsed;
68
+ }
69
+ //# sourceMappingURL=export-operation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-operation.js","sourceRoot":"","sources":["../../src/export/export-operation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ export interface LegacyExportSnapshot {
2
+ users: LegacyUser[];
3
+ categories: LegacyCategory[];
4
+ tags: LegacyTag[];
5
+ posts: LegacyPost[];
6
+ postTags: LegacyPostTag[];
7
+ comments: LegacyComment[];
8
+ friendLinks: LegacyFriendLink[];
9
+ keyGroups: LegacyKeyGroup[];
10
+ keyCredentials: LegacyKeyCredential[];
11
+ siteConfig: Record<string, unknown>;
12
+ }
13
+ export interface LegacyUser {
14
+ id: number;
15
+ email: string;
16
+ forbidden: boolean;
17
+ nickname: string | null;
18
+ avatarUrl: string | null;
19
+ homepageUrl: string | null;
20
+ createdAt: Date;
21
+ updatedAt: Date;
22
+ }
23
+ export interface LegacyCategory {
24
+ id: number;
25
+ slug: string;
26
+ name: string;
27
+ description: string | null;
28
+ sortOrder: number;
29
+ }
30
+ export interface LegacyTag {
31
+ id: number;
32
+ slug: string;
33
+ name: string;
34
+ }
35
+ export interface LegacyPostTag {
36
+ postId: number;
37
+ tagId: number;
38
+ }
39
+ export interface LegacyPost {
40
+ id: number;
41
+ shortId: string;
42
+ title: string;
43
+ excerpt: string | null;
44
+ content: string;
45
+ status: 'draft' | 'published';
46
+ categoryId: number;
47
+ publishedAt: Date | null;
48
+ createdAt: Date;
49
+ updatedAt: Date;
50
+ }
51
+ export interface LegacyComment {
52
+ id: number;
53
+ postId: number;
54
+ userId: number;
55
+ content: string;
56
+ reply: string | null;
57
+ status: 'pending' | 'processing' | 'approved' | 'rejected' | 'spam';
58
+ ip: string;
59
+ userAgent: string;
60
+ createdAt: Date;
61
+ }
62
+ export interface LegacyFriendLink {
63
+ id: number;
64
+ name: string;
65
+ url: string;
66
+ description: string | null;
67
+ logoUrl: string | null;
68
+ sortOrder: number;
69
+ status: string;
70
+ createdAt: Date;
71
+ updatedAt: Date;
72
+ }
73
+ export interface LegacyKeyGroup {
74
+ id: number;
75
+ ownerUserId: number;
76
+ parentId: number | null;
77
+ name: string;
78
+ description: string | null;
79
+ sortOrder: number;
80
+ createdAt: Date;
81
+ updatedAt: Date;
82
+ }
83
+ export interface LegacyKeyCredential {
84
+ id: number;
85
+ ownerUserId: number;
86
+ groupId: number;
87
+ title: string;
88
+ account: string | null;
89
+ remark: string | null;
90
+ publicKeyPem: string;
91
+ privateKeyPem: string;
92
+ vaultPasswordCipher: string | null;
93
+ sortOrder: number;
94
+ createdAt: Date;
95
+ updatedAt: Date;
96
+ }
97
+ export interface LegacyExportDocument {
98
+ readonly users: readonly Record<string, unknown>[];
99
+ readonly categories: readonly Record<string, unknown>[];
100
+ readonly tags: readonly Record<string, unknown>[];
101
+ readonly posts: readonly ExportPost[];
102
+ readonly comments: readonly Record<string, unknown>[];
103
+ readonly friendLinks: readonly Record<string, unknown>[];
104
+ readonly siteConfig: Record<string, unknown>;
105
+ readonly keyVault: {
106
+ readonly schemaVersion: 1;
107
+ readonly groups: readonly Record<string, unknown>[];
108
+ readonly credentials: readonly ExportKeyCredential[];
109
+ };
110
+ readonly report: {
111
+ readonly approximateReplyTimestampCommentIds: readonly number[];
112
+ readonly excludedLegacyFields: readonly string[];
113
+ };
114
+ }
115
+ interface ExportPost extends Record<string, unknown> {
116
+ readonly legacyId: number;
117
+ readonly slug: string;
118
+ readonly contentFile: string;
119
+ readonly content: string;
120
+ }
121
+ interface ExportKeyCredential extends Record<string, unknown> {
122
+ readonly legacyId: number;
123
+ readonly vaultPassword: string | null;
124
+ }
125
+ export declare function buildLegacyExport(source: LegacyExportSnapshot): LegacyExportDocument;
126
+ export declare function writeLegacyExport(exportDirectory: string, document: LegacyExportDocument, exportedAt?: Date): Promise<void>;
127
+ export declare function openLegacyVaultPassword(sealedBase64Url: string, privateKeyPem: string, publicKeyPem: string): string;
128
+ export {};
129
+ //# sourceMappingURL=legacy-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-export.d.ts","sourceRoot":"","sources":["../../src/export/legacy-export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,334 @@
1
+ import { createDecipheriv, createHash, createPrivateKey, createPublicKey, } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { access, chmod, mkdir, mkdtemp, rename, rm, writeFile, } from 'node:fs/promises';
4
+ import { basename, dirname, join } from 'node:path';
5
+ const KDF_DOMAIN = Buffer.from('zlooks:user-key-vault:vault-password-seal:v1', 'utf8');
6
+ const SEAL_VERSION = 0x01;
7
+ const IV_LENGTH = 12;
8
+ const TAG_LENGTH = 16;
9
+ const HEADER_LENGTH = 1 + IV_LENGTH + TAG_LENGTH;
10
+ export function buildLegacyExport(source) {
11
+ validateRelationships(source);
12
+ const postTags = groupPostTags(source.postTags);
13
+ const posts = source.posts.map((post) => projectPost(post, postTags.get(post.id) ?? []));
14
+ assertUniquePostFiles(posts);
15
+ return {
16
+ users: source.users.map((user) => ({
17
+ legacyId: user.id,
18
+ displayName: user.nickname,
19
+ avatar: user.avatarUrl,
20
+ homepage: user.homepageUrl,
21
+ bio: null,
22
+ email: user.email.trim().normalize('NFKC').toLocaleLowerCase('en-US'),
23
+ emailVerified: false,
24
+ status: user.forbidden ? 'disabled' : 'active',
25
+ createdAt: user.createdAt.toISOString(),
26
+ updatedAt: user.updatedAt.toISOString(),
27
+ })),
28
+ categories: source.categories.map((category) => ({
29
+ legacyId: category.id,
30
+ name: category.name,
31
+ slug: category.slug,
32
+ description: category.description,
33
+ sortOrder: category.sortOrder,
34
+ })),
35
+ tags: source.tags.map((tag) => ({ legacyId: tag.id, name: tag.name, slug: tag.slug })),
36
+ posts,
37
+ comments: source.comments.map(projectComment),
38
+ friendLinks: source.friendLinks.map((link) => ({
39
+ legacyId: link.id,
40
+ name: link.name,
41
+ url: link.url,
42
+ description: link.description,
43
+ logoUrl: link.logoUrl,
44
+ enabled: link.status === 'approved',
45
+ sortOrder: link.sortOrder,
46
+ createdAt: link.createdAt.toISOString(),
47
+ updatedAt: link.updatedAt.toISOString(),
48
+ })),
49
+ siteConfig: projectSiteConfig(source.siteConfig),
50
+ keyVault: {
51
+ schemaVersion: 1,
52
+ groups: source.keyGroups.map((group) => ({
53
+ legacyId: group.id,
54
+ ownerLegacyUserId: group.ownerUserId,
55
+ parentLegacyId: group.parentId,
56
+ name: group.name,
57
+ description: group.description,
58
+ sortOrder: group.sortOrder,
59
+ createdAt: group.createdAt.toISOString(),
60
+ updatedAt: group.updatedAt.toISOString(),
61
+ })),
62
+ credentials: source.keyCredentials.map(projectKeyCredential),
63
+ },
64
+ report: {
65
+ approximateReplyTimestampCommentIds: source.comments
66
+ .filter((comment) => comment.reply !== null)
67
+ .map((comment) => comment.id),
68
+ excludedLegacyFields: [
69
+ 'users.password_hash',
70
+ 'users.role',
71
+ 'users.phone',
72
+ 'posts.comments_disabled',
73
+ 'post_comments.is_bot',
74
+ 'friend_links.message',
75
+ 'friend_links.submitter_id',
76
+ ],
77
+ },
78
+ };
79
+ }
80
+ export async function writeLegacyExport(exportDirectory, document, exportedAt = new Date()) {
81
+ if (await pathExists(exportDirectory))
82
+ throw new Error(`导出目录已存在:${exportDirectory}`);
83
+ const parent = dirname(exportDirectory);
84
+ await mkdir(parent, { recursive: true, mode: 0o700 });
85
+ const temporaryDirectory = await mkdtemp(join(parent, `.${basename(exportDirectory)}-`));
86
+ await chmod(temporaryDirectory, 0o700);
87
+ try {
88
+ await writeExportFiles(temporaryDirectory, document, exportedAt);
89
+ await rename(temporaryDirectory, exportDirectory);
90
+ }
91
+ catch (error) {
92
+ await rm(temporaryDirectory, { recursive: true, force: true });
93
+ throw error;
94
+ }
95
+ }
96
+ export function openLegacyVaultPassword(sealedBase64Url, privateKeyPem, publicKeyPem) {
97
+ const raw = Buffer.from(sealedBase64Url.trim(), 'base64url');
98
+ if (raw.length < HEADER_LENGTH + 1)
99
+ throw new Error('保管口令密文长度过短');
100
+ if (raw[0] !== SEAL_VERSION)
101
+ throw new Error('保管口令密文版本不支持');
102
+ const iv = raw.subarray(1, 1 + IV_LENGTH);
103
+ const tag = raw.subarray(1 + IV_LENGTH, HEADER_LENGTH);
104
+ const ciphertext = raw.subarray(HEADER_LENGTH);
105
+ const decipher = createDecipheriv('aes-256-gcm', deriveVaultKey(privateKeyPem, publicKeyPem), iv);
106
+ decipher.setAuthTag(tag);
107
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
108
+ }
109
+ function projectPost(post, tagLegacyIds) {
110
+ const stem = safePostFileStem(post.shortId, post.id);
111
+ return {
112
+ legacyId: post.id,
113
+ title: post.title,
114
+ slug: post.shortId,
115
+ summary: post.excerpt,
116
+ coverUrl: null,
117
+ contentFile: `${stem}.md`,
118
+ content: post.content,
119
+ status: post.status,
120
+ categoryLegacyId: post.categoryId,
121
+ tagLegacyIds: [...tagLegacyIds],
122
+ publishedAt: post.publishedAt?.toISOString() ?? null,
123
+ createdAt: post.createdAt.toISOString(),
124
+ updatedAt: post.updatedAt.toISOString(),
125
+ };
126
+ }
127
+ function projectComment(comment) {
128
+ return {
129
+ legacyId: comment.id,
130
+ postLegacyId: comment.postId,
131
+ userLegacyId: comment.userId,
132
+ content: comment.content,
133
+ ipAddress: comment.ip || null,
134
+ userAgent: comment.userAgent || null,
135
+ ipLocation: null,
136
+ clientLabel: null,
137
+ replyContent: comment.reply,
138
+ repliedAt: comment.reply === null ? null : comment.createdAt.toISOString(),
139
+ status: normalizeCommentStatus(comment.status),
140
+ createdAt: comment.createdAt.toISOString(),
141
+ updatedAt: comment.createdAt.toISOString(),
142
+ };
143
+ }
144
+ function projectKeyCredential(credential) {
145
+ let vaultPassword = null;
146
+ if (credential.vaultPasswordCipher?.trim()) {
147
+ try {
148
+ vaultPassword = openLegacyVaultPassword(credential.vaultPasswordCipher, credential.privateKeyPem, credential.publicKeyPem);
149
+ }
150
+ catch (error) {
151
+ throw new Error(`密钥凭据 ${credential.id} 的保管密码解密失败`, { cause: error });
152
+ }
153
+ }
154
+ return {
155
+ legacyId: credential.id,
156
+ ownerLegacyUserId: credential.ownerUserId,
157
+ groupLegacyId: credential.groupId,
158
+ title: credential.title,
159
+ account: credential.account,
160
+ remark: credential.remark,
161
+ publicKeyPem: credential.publicKeyPem,
162
+ privateKeyPem: credential.privateKeyPem,
163
+ vaultPassword,
164
+ sortOrder: credential.sortOrder,
165
+ createdAt: credential.createdAt.toISOString(),
166
+ updatedAt: credential.updatedAt.toISOString(),
167
+ };
168
+ }
169
+ function projectSiteConfig(config) {
170
+ const title = optionalString(config.site_title) ?? 'ZLOOKS';
171
+ const description = optionalString(config.site_description);
172
+ const copyright = optionalString(config.site_copyright);
173
+ const registration = optionalString(config.site_icp_record);
174
+ return {
175
+ schemaVersion: 1,
176
+ sitePresentation: {
177
+ identity: {
178
+ name: title,
179
+ homeHref: '/',
180
+ ...(description ? { description } : {}),
181
+ },
182
+ linkGroups: [],
183
+ legal: {
184
+ ...(copyright ? { copyright } : {}),
185
+ registrations: registration ? [{ id: 'legacy-icp', kind: 'icp', number: registration }] : [],
186
+ },
187
+ },
188
+ };
189
+ }
190
+ async function writeExportFiles(directory, document, exportedAt) {
191
+ const postsDirectory = join(directory, 'posts');
192
+ await mkdir(postsDirectory, { mode: 0o700 });
193
+ const publicFiles = [
194
+ ['users.json', document.users],
195
+ ['categories.json', document.categories],
196
+ ['tags.json', document.tags],
197
+ ['comments.json', document.comments],
198
+ ['friend-links.json', document.friendLinks],
199
+ ['site-config.json', document.siteConfig],
200
+ ['key-vault.json', document.keyVault],
201
+ ['report.json', document.report],
202
+ ];
203
+ for (const [name, value] of publicFiles)
204
+ await writeJson(join(directory, name), value);
205
+ for (const post of document.posts) {
206
+ const { content, ...metadata } = post;
207
+ await writeJson(join(postsDirectory, `${safePostFileStem(post.slug, post.legacyId)}.json`), metadata);
208
+ await writePrivateFile(join(postsDirectory, post.contentFile), content);
209
+ }
210
+ await writeJson(join(directory, 'manifest.json'), {
211
+ schemaVersion: 1,
212
+ source: 'zlooks.cn',
213
+ exportedAt: exportedAt.toISOString(),
214
+ counts: {
215
+ users: document.users.length,
216
+ categories: document.categories.length,
217
+ tags: document.tags.length,
218
+ posts: document.posts.length,
219
+ comments: document.comments.length,
220
+ friendLinks: document.friendLinks.length,
221
+ keyGroups: document.keyVault.groups.length,
222
+ keyCredentials: document.keyVault.credentials.length,
223
+ },
224
+ });
225
+ }
226
+ async function writeJson(path, value) {
227
+ await writePrivateFile(path, `${JSON.stringify(value, null, 2)}\n`);
228
+ }
229
+ async function writePrivateFile(path, contents) {
230
+ await writeFile(path, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
231
+ await chmod(path, 0o600);
232
+ }
233
+ function groupPostTags(rows) {
234
+ const result = new Map();
235
+ for (const row of rows) {
236
+ const tags = result.get(row.postId) ?? [];
237
+ tags.push(row.tagId);
238
+ result.set(row.postId, tags);
239
+ }
240
+ for (const tags of result.values())
241
+ tags.sort((left, right) => left - right);
242
+ return result;
243
+ }
244
+ function assertUniquePostFiles(posts) {
245
+ const files = new Set();
246
+ for (const post of posts) {
247
+ if (files.has(post.contentFile))
248
+ throw new Error(`文章导出文件名冲突:${post.contentFile}`);
249
+ files.add(post.contentFile);
250
+ }
251
+ }
252
+ function safePostFileStem(shortId, legacyId) {
253
+ const normalized = shortId.trim();
254
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(normalized) ? normalized : `legacy-${legacyId}`;
255
+ }
256
+ function normalizeCommentStatus(status) {
257
+ if (status === 'approved')
258
+ return 'approved';
259
+ if (status === 'rejected' || status === 'spam')
260
+ return 'rejected';
261
+ if (status === 'pending' || status === 'processing')
262
+ return 'pending';
263
+ throw new Error(`评论状态无法导出:${String(status)}`);
264
+ }
265
+ function validateRelationships(source) {
266
+ const userIds = ids(source.users);
267
+ const categoryIds = ids(source.categories);
268
+ const tagIds = ids(source.tags);
269
+ const postIds = ids(source.posts);
270
+ const groupById = new Map(source.keyGroups.map((group) => [group.id, group]));
271
+ for (const post of source.posts) {
272
+ assertReference(categoryIds, post.categoryId, `文章 ${post.id} 引用了不存在的分类`);
273
+ if (post.status !== 'draft' && post.status !== 'published') {
274
+ throw new Error(`文章 ${post.id} 的状态无法导出:${String(post.status)}`);
275
+ }
276
+ }
277
+ for (const row of source.postTags) {
278
+ assertReference(postIds, row.postId, `文章标签关系引用了不存在的文章`);
279
+ assertReference(tagIds, row.tagId, `文章 ${row.postId} 引用了不存在的标签`);
280
+ }
281
+ for (const comment of source.comments) {
282
+ assertReference(postIds, comment.postId, `评论 ${comment.id} 引用了不存在的文章`);
283
+ assertReference(userIds, comment.userId, `评论 ${comment.id} 引用了不存在的用户`);
284
+ }
285
+ for (const group of source.keyGroups) {
286
+ assertReference(userIds, group.ownerUserId, `密钥分组 ${group.id} 引用了不存在的用户`);
287
+ if (group.parentId !== null)
288
+ assertReference(groupById, group.parentId, `密钥分组 ${group.id} 引用了不存在的父分组`);
289
+ }
290
+ for (const credential of source.keyCredentials) {
291
+ assertReference(userIds, credential.ownerUserId, `密钥凭据 ${credential.id} 引用了不存在的用户`);
292
+ const group = groupById.get(credential.groupId);
293
+ if (!group)
294
+ throw new Error(`密钥凭据 ${credential.id} 引用了不存在的分组 ${credential.groupId}`);
295
+ if (group.ownerUserId !== credential.ownerUserId) {
296
+ throw new Error(`密钥凭据 ${credential.id} 与分组 ${credential.groupId} 的所有者不一致`);
297
+ }
298
+ }
299
+ }
300
+ function ids(rows) {
301
+ return new Set(rows.map((row) => row.id));
302
+ }
303
+ function assertReference(collection, id, message) {
304
+ if (!collection.has(id))
305
+ throw new Error(`${message} ${id}`);
306
+ }
307
+ function deriveVaultKey(privateKeyPem, publicKeyPem) {
308
+ const privateDer = createPrivateKey({ key: privateKeyPem, format: 'pem', type: 'pkcs8' })
309
+ .export({ format: 'der', type: 'pkcs8' });
310
+ const publicDer = createPublicKey({ key: publicKeyPem, format: 'pem', type: 'spki' })
311
+ .export({ format: 'der', type: 'spki' });
312
+ return createHash('sha256').update(KDF_DOMAIN).update(privateDer).update(publicDer).digest();
313
+ }
314
+ function optionalString(value) {
315
+ if (typeof value !== 'string')
316
+ return undefined;
317
+ const trimmed = value.trim();
318
+ return trimmed || undefined;
319
+ }
320
+ async function pathExists(path) {
321
+ try {
322
+ await access(path, constants.F_OK);
323
+ return true;
324
+ }
325
+ catch (error) {
326
+ if (isNodeError(error) && error.code === 'ENOENT')
327
+ return false;
328
+ throw error;
329
+ }
330
+ }
331
+ function isNodeError(error) {
332
+ return error instanceof Error && 'code' in error;
333
+ }
334
+ //# sourceMappingURL=legacy-export.js.map