@southwind-ai/shared 0.1.0 → 0.1.2

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/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./src/feature.js";
5
5
  export * from "./src/license.js";
6
6
  export * from "./src/license-catalog.js";
7
7
  export * from "./src/migration.js";
8
+ export * from "./src/migration-bundle-validation.js";
8
9
  export * from "./src/module-configuration.js";
9
10
  export * from "./src/correlation.js";
10
11
  export * from "./src/notification.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/shared",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,6 +7,8 @@ export interface LicenseCatalogDefinition {
7
7
  order: number;
8
8
  }
9
9
 
10
+ const licenseCatalogFields = new Set(["key", "label", "description", "order"]);
11
+
10
12
  export const LICENSE_CATALOG = [
11
13
  {
12
14
  key: licenseVersionKey("free-trial"),
@@ -31,3 +33,96 @@ export const LICENSE_CATALOG = [
31
33
  export function licenseCatalogKeys(): ReadonlySet<LicenseVersionKey> {
32
34
  return new Set(LICENSE_CATALOG.map(({ key }) => key));
33
35
  }
36
+
37
+ /** 校验宿主配置的 License 版本目录:Key 与 order 均唯一,目录非空。 */
38
+ export function validateLicenseCatalog(
39
+ definitions: readonly LicenseCatalogDefinition[],
40
+ ): void {
41
+ if (definitions.length === 0) {
42
+ throw new Error("License 版本目录不能为空");
43
+ }
44
+ const keys = new Set<string>();
45
+ const orders = new Set<number>();
46
+ for (const definition of definitions) {
47
+ if (!definition.key.trim() || !definition.label.trim()) {
48
+ throw new Error("License 版本目录的 Key 与名称不能为空");
49
+ }
50
+ if (!Number.isSafeInteger(definition.order)) {
51
+ throw new Error(`License 版本 ${definition.key} 的 order 必须是安全整数`);
52
+ }
53
+ if (keys.has(definition.key)) {
54
+ throw new Error(`License 版本目录 Key 重复:${definition.key}`);
55
+ }
56
+ if (orders.has(definition.order)) {
57
+ throw new Error(
58
+ `License 版本目录 order 重复:${definition.order}(${definition.key})`,
59
+ );
60
+ }
61
+ keys.add(definition.key);
62
+ orders.add(definition.order);
63
+ }
64
+ }
65
+
66
+ /** 从未知输入解析并归一化宿主 License 版本目录。 */
67
+ export function parseLicenseCatalog(
68
+ input: unknown,
69
+ ): LicenseCatalogDefinition[] {
70
+ if (!Array.isArray(input)) {
71
+ throw new Error("License 版本目录必须是数组");
72
+ }
73
+ const catalog = input.map((entry, index) => {
74
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
75
+ throw new Error(`License 版本目录第 ${index + 1} 项必须是对象`);
76
+ }
77
+ const record = entry as Record<string, unknown>;
78
+ const unknownField = Object.keys(record).find(
79
+ (key) => !licenseCatalogFields.has(key),
80
+ );
81
+ if (unknownField) {
82
+ throw new Error(`License 版本目录包含未知字段:${unknownField}`);
83
+ }
84
+ if (
85
+ typeof record.key !== "string" ||
86
+ typeof record.label !== "string" ||
87
+ (record.description !== undefined &&
88
+ typeof record.description !== "string") ||
89
+ typeof record.order !== "number"
90
+ ) {
91
+ throw new Error(`License 版本目录第 ${index + 1} 项字段类型无效`);
92
+ }
93
+ return {
94
+ key: licenseVersionKey(record.key.trim()),
95
+ label: record.label.trim(),
96
+ description: record.description?.trim() ?? "",
97
+ order: record.order,
98
+ };
99
+ });
100
+ validateLicenseCatalog(catalog);
101
+ return catalog;
102
+ }
103
+
104
+ /** 解析环境变量 JSON,错误消息不包含原始配置内容。 */
105
+ export function parseLicenseCatalogJson(
106
+ raw: string,
107
+ source = "License 版本目录",
108
+ ): LicenseCatalogDefinition[] {
109
+ let parsed: unknown;
110
+ try {
111
+ parsed = JSON.parse(raw);
112
+ } catch {
113
+ throw new Error(`${source} 必须是合法 JSON 数组`);
114
+ }
115
+ if (!Array.isArray(parsed)) {
116
+ throw new Error(`${source} 必须是合法 JSON 数组`);
117
+ }
118
+ return parseLicenseCatalog(parsed);
119
+ }
120
+
121
+ /** 有宿主配置时校验并返回,未配置时回退平台默认三级目录。 */
122
+ export function resolveLicenseCatalog(
123
+ configured?: readonly LicenseCatalogDefinition[],
124
+ ): readonly LicenseCatalogDefinition[] {
125
+ if (!configured) return LICENSE_CATALOG;
126
+ validateLicenseCatalog(configured);
127
+ return configured;
128
+ }
@@ -0,0 +1,219 @@
1
+ import {
2
+ MIGRATION_BUNDLE_FORMAT_VERSION,
3
+ type BundleMigrationDefinition,
4
+ type MigrationBundle,
5
+ type MigrationDefinition,
6
+ type MigrationManifestSource,
7
+ } from "./migration.js";
8
+
9
+ export function normalizeManifestBundles(
10
+ modules: readonly MigrationManifestSource[],
11
+ ): MigrationBundle[] {
12
+ return modules.map((manifest) => {
13
+ const bundle =
14
+ manifest.migrationBundle ||
15
+ legacyManifestBundle(manifest, manifest.migrations);
16
+ if (bundle.module !== manifest.name) {
17
+ throw new Error(
18
+ `Migration Bundle ${bundle.module} 与 Manifest ${manifest.name} 不一致`,
19
+ );
20
+ }
21
+ if (bundle.version !== manifest.version) {
22
+ throw new Error(
23
+ `Migration Bundle ${bundle.module} 版本 ${bundle.version} 与 Manifest ${manifest.version} 不一致`,
24
+ );
25
+ }
26
+ assertSameDependencies(manifest, bundle);
27
+ assertSameMigrations(manifest, bundle);
28
+ assertSchemaMigrationReferences(manifest, bundle);
29
+ return bundle;
30
+ });
31
+ }
32
+
33
+ function assertSameDependencies(
34
+ manifest: MigrationManifestSource,
35
+ bundle: MigrationBundle,
36
+ ): void {
37
+ const declared = manifest.dependencies || [];
38
+ const bundled = bundle.dependencies || [];
39
+ if (
40
+ declared.length !== bundled.length ||
41
+ declared.some((dependency) => !bundled.includes(dependency))
42
+ ) {
43
+ throw new Error(
44
+ `Manifest ${manifest.name} 的 dependencies 必须与 Migration Bundle 一致`,
45
+ );
46
+ }
47
+ }
48
+
49
+ function legacyManifestBundle(
50
+ manifest: MigrationManifestSource,
51
+ migrations: readonly MigrationDefinition[],
52
+ ): MigrationBundle {
53
+ return {
54
+ formatVersion: MIGRATION_BUNDLE_FORMAT_VERSION,
55
+ module: manifest.name,
56
+ version: manifest.version,
57
+ dependencies: manifest.dependencies,
58
+ migrations: migrations.map((migration) => ({
59
+ ...migration,
60
+ checksum:
61
+ migration.checksum ||
62
+ `legacy:${manifest.name}:${manifest.version}:${migration.id}`,
63
+ })),
64
+ };
65
+ }
66
+
67
+ function assertSameMigrations(
68
+ manifest: MigrationManifestSource,
69
+ bundle: MigrationBundle,
70
+ ): void {
71
+ const declared = manifest.migrations.map(({ id }) => id);
72
+ const bundled = bundle.migrations.map(({ id }) => id);
73
+ if (
74
+ declared.length !== bundled.length ||
75
+ declared.some((id, index) => id !== bundled[index])
76
+ ) {
77
+ throw new Error(
78
+ `Manifest ${manifest.name} 的 migrations 必须与 Migration Bundle 完全一致`,
79
+ );
80
+ }
81
+ }
82
+
83
+ function assertSchemaMigrationReferences(
84
+ manifest: MigrationManifestSource,
85
+ bundle: MigrationBundle,
86
+ ): void {
87
+ const ids = new Set(bundle.migrations.map(({ id }) => id));
88
+ for (const schema of manifest.schemaExports || []) {
89
+ for (const migrationId of schema.migrationIds) {
90
+ if (!ids.has(migrationId)) {
91
+ throw new Error(
92
+ `Schema Export ${schema.key} 引用了 Bundle 中不存在的 Migration:${migrationId}`,
93
+ );
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ export function validateAndOrderBundles(
100
+ bundles: readonly MigrationBundle[],
101
+ ): MigrationBundle[] {
102
+ const byModule = new Map<string, MigrationBundle>();
103
+ const migrationOwner = new Map<string, string>();
104
+ for (const bundle of bundles) {
105
+ validateBundle(bundle);
106
+ if (byModule.has(bundle.module)) {
107
+ throw new Error(`Migration Bundle 模块重复:${bundle.module}`);
108
+ }
109
+ byModule.set(bundle.module, bundle);
110
+ for (const migration of bundle.migrations) {
111
+ const owner = migrationOwner.get(migration.id);
112
+ if (owner) {
113
+ throw new Error(
114
+ `Migration ID 重复:${migration.id}(${owner} / ${bundle.module})`,
115
+ );
116
+ }
117
+ migrationOwner.set(migration.id, bundle.module);
118
+ }
119
+ }
120
+ for (const bundle of bundles) {
121
+ for (const dependency of bundle.dependencies || []) {
122
+ if (!byModule.has(dependency)) {
123
+ throw new Error(
124
+ `Migration Bundle ${bundle.module} 依赖未安装模块:${dependency}`,
125
+ );
126
+ }
127
+ }
128
+ validateMigrationDependencies(bundle, migrationOwner);
129
+ orderBundleMigrations(bundle);
130
+ }
131
+ return topologicalBundles(byModule);
132
+ }
133
+
134
+ function validateBundle(bundle: MigrationBundle): void {
135
+ if (bundle.formatVersion !== MIGRATION_BUNDLE_FORMAT_VERSION) {
136
+ throw new Error(
137
+ `Migration Bundle ${bundle.module} 格式版本不受支持:${bundle.formatVersion}`,
138
+ );
139
+ }
140
+ if (!bundle.module.trim()) throw new Error("Migration Bundle 缺少 module");
141
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(bundle.version)) {
142
+ throw new Error(
143
+ `Migration Bundle ${bundle.module} 版本无效:${bundle.version}`,
144
+ );
145
+ }
146
+ for (const migration of bundle.migrations) {
147
+ if (migration.module !== bundle.module) {
148
+ throw new Error(
149
+ `Migration ${migration.id} 的模块归属应为 ${bundle.module}`,
150
+ );
151
+ }
152
+ if (!migration.id.trim()) throw new Error("Migration ID 不能为空");
153
+ if (!migration.checksum.trim()) {
154
+ throw new Error(`Migration ${migration.id} 缺少 checksum`);
155
+ }
156
+ }
157
+ }
158
+
159
+ function validateMigrationDependencies(
160
+ bundle: MigrationBundle,
161
+ owners: ReadonlyMap<string, string>,
162
+ ): void {
163
+ for (const migration of bundle.migrations) {
164
+ for (const dependency of migration.dependsOn || []) {
165
+ const owner = owners.get(dependency);
166
+ if (!owner) {
167
+ throw new Error(`Migration ${migration.id} 依赖不存在:${dependency}`);
168
+ }
169
+ if (
170
+ owner !== bundle.module &&
171
+ !(bundle.dependencies || []).includes(owner)
172
+ ) {
173
+ throw new Error(
174
+ `Migration ${migration.id} 跨模块依赖 ${owner},但 Bundle 未声明该模块依赖`,
175
+ );
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ function topologicalBundles(
182
+ pendingSource: ReadonlyMap<string, MigrationBundle>,
183
+ ): MigrationBundle[] {
184
+ const pending = new Map(pendingSource);
185
+ const sorted: MigrationBundle[] = [];
186
+ while (pending.size > 0) {
187
+ const ready = Array.from(pending.values()).find((bundle) =>
188
+ (bundle.dependencies || []).every(
189
+ (dependency) => !pending.has(dependency),
190
+ ),
191
+ );
192
+ if (!ready) throw new Error("Migration Bundle 模块依赖存在循环");
193
+ sorted.push(ready);
194
+ pending.delete(ready.module);
195
+ }
196
+ return sorted;
197
+ }
198
+
199
+ export function orderBundleMigrations(
200
+ bundle: MigrationBundle,
201
+ ): BundleMigrationDefinition[] {
202
+ const pending = new Map(
203
+ bundle.migrations.map((migration) => [migration.id, migration]),
204
+ );
205
+ const sorted: BundleMigrationDefinition[] = [];
206
+ while (pending.size > 0) {
207
+ const ready = Array.from(pending.values()).find((migration) =>
208
+ (migration.dependsOn || []).every(
209
+ (dependency) => !pending.has(dependency),
210
+ ),
211
+ );
212
+ if (!ready) {
213
+ throw new Error(`Migration Bundle ${bundle.module} 的迁移依赖存在循环`);
214
+ }
215
+ sorted.push(ready);
216
+ pending.delete(ready.id);
217
+ }
218
+ return sorted;
219
+ }
@@ -3,3 +3,16 @@ export interface PlatformBrandingSettings {
3
3
  logoUrl?: string;
4
4
  description?: string;
5
5
  }
6
+
7
+ export interface PlatformWatermarkPolicy {
8
+ enabled: boolean;
9
+ businessPagesEnabled: boolean;
10
+ customContent: string;
11
+ }
12
+
13
+ export const DEFAULT_PLATFORM_WATERMARK_POLICY: Readonly<PlatformWatermarkPolicy> =
14
+ {
15
+ enabled: true,
16
+ businessPagesEnabled: false,
17
+ customContent: "",
18
+ };