create-windy 0.6.8 → 0.6.10

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 (80) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +1012 -329
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +3 -3
  5. package/template/README.md +8 -2
  6. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
  7. package/template/apps/agent-server/tests/test_multimodal.py +30 -0
  8. package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
  9. package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
  10. package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
  11. package/template/apps/server/src/agent/run-audit.ts +1 -0
  12. package/template/apps/server/src/agent/run-contracts.ts +1 -0
  13. package/template/apps/server/src/agent/run-facade.ts +2 -2
  14. package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
  15. package/template/apps/server/src/agent/run-output-schema.ts +15 -0
  16. package/template/apps/server/src/agent/run-release.ts +11 -0
  17. package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
  18. package/template/apps/server/src/agent/vision-input.ts +37 -2
  19. package/template/apps/server/src/license/pairing-http.ts +138 -0
  20. package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
  21. package/template/apps/server/src/license/pairing-routes.ts +257 -0
  22. package/template/apps/server/src/license/pairing-service.test.ts +320 -0
  23. package/template/apps/server/src/license/pairing-service.ts +321 -0
  24. package/template/apps/server/src/license/restriction.test.ts +11 -0
  25. package/template/apps/server/src/license/restriction.ts +5 -0
  26. package/template/apps/server/src/license/routes.ts +8 -1
  27. package/template/apps/server/src/license/runtime-service.ts +20 -0
  28. package/template/apps/server/src/runtime-development.ts +1 -0
  29. package/template/apps/web/package.json +6 -4
  30. package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
  31. package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
  32. package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
  33. package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
  34. package/template/apps/web/src/layout/AppSidebar.vue +1 -0
  35. package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
  36. package/template/apps/web/src/pages/ui/UiComponentPage.vue +7 -0
  37. package/template/apps/web/src/pages/ui/UiComponentPage.webtest.ts +94 -0
  38. package/template/apps/web/src/pages/ui/component-catalog.ts +248 -0
  39. package/template/apps/web/src/pages/ui/components/ComponentPreviewCard.vue +50 -0
  40. package/template/apps/web/src/pages/ui/components/DataComponentShowcases.vue +131 -0
  41. package/template/apps/web/src/pages/ui/components/FeedbackComponentShowcases.vue +158 -0
  42. package/template/apps/web/src/pages/ui/components/FoundationComponentShowcases.vue +129 -0
  43. package/template/apps/web/src/pages/ui/components/NavigationComponentShowcases.vue +155 -0
  44. package/template/apps/web/src/pages/ui/components/ProjectComponentShowcases.vue +31 -0
  45. package/template/apps/web/src/pages/ui/components/UiComponentGallery.vue +238 -0
  46. package/template/apps/web/src/router/index.ts +2 -0
  47. package/template/apps/web/src/router/manifest-routes.webtest.ts +1 -0
  48. package/template/apps/web/src/router/platform-admin-route-bindings.ts +3 -1
  49. package/template/apps/web/src/services/license-api.ts +60 -0
  50. package/template/apps/web/src/style.webtest.ts +5 -6
  51. package/template/docs/architecture/ai-runtime.md +5 -0
  52. package/template/docs/development/business-web-routes.md +3 -1
  53. package/template/docs/development/ui-component-gallery.md +44 -0
  54. package/template/docs/license/license-pairing-activation.md +67 -0
  55. package/template/docs/operations/incremental-delivery.md +10 -9
  56. package/template/docs/platform/agent-supervisor-output-contract.md +80 -0
  57. package/template/packages/ai-client/package.json +1 -1
  58. package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
  59. package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
  60. package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
  61. package/template/packages/modules/index.ts +1 -0
  62. package/template/packages/modules/package.json +1 -1
  63. package/template/packages/modules/src/system-admin-routes.ts +8 -0
  64. package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -2
  65. package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
  66. package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
  67. package/template/packages/modules/src/system-api-control-plane.ts +37 -0
  68. package/template/packages/modules/src/system-features.ts +5 -0
  69. package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
  70. package/template/packages/modules/src/system-module-catalog.ts +10 -19
  71. package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
  72. package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
  73. package/template/packages/modules/src/system-modules.ts +13 -0
  74. package/template/packages/modules/src/system-permissions.ts +2 -0
  75. package/template/packages/modules/src/system-ui-components.test.ts +36 -0
  76. package/template/packages/server-sdk/index.ts +5 -0
  77. package/template/packages/server-sdk/package.json +3 -1
  78. package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
  79. package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
  80. package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "47527e1bb3a2848bbb7d27eef7132c83b81d714d",
4
- "generatorVersion": "0.6.8",
3
+ "templateCommit": "1916707e888868d817a115dc8a96244a28e41d2c",
4
+ "generatorVersion": "0.6.10",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  {
35
35
  "name": "system.agent",
36
- "version": "0.1.0"
36
+ "version": "0.1.1"
37
37
  },
38
38
  {
39
39
  "name": "system.operations",
@@ -35,6 +35,10 @@ Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接
35
35
  BuildKit 中的 Bun 包下载缓存,只下载新增或发生变化的依赖;冷安装每 15 秒输出一次
36
36
  不含 registry 地址或凭据的进度心跳。
37
37
 
38
+ Windows 10/11 可在原生 PowerShell 中配合 Docker Desktop 直接运行 `bun run dev` 与
39
+ `bun run dev:build`,不需要 WSL。Windows 是本地开发宿主;生产交付仍使用 Linux
40
+ amd64 容器。
41
+
38
42
  | 服务 | 地址 |
39
43
  | ---- | ---- |
40
44
  | Web | http://localhost:18746 |
@@ -132,7 +136,8 @@ browser-safe `./admin` 与 project-owned 的
132
136
  `apps/web/src/app/web-composition.ts` 装配页面,不复制第二份 Feature、Permission
133
137
  或路由清单。整个 `apps/web/src` 与整个 `apps/server/src/app` 都是
134
138
  project-owned,客户可以修改 Router、layout、页面、Host 和 Server 组合;普通
135
- `windy update` 不会覆盖或三方合并这些文件。
139
+ `windy update` 不会整文件覆盖或三方合并这些文件。版本 recipe 只有在显式登记
140
+ path 与确定性语义锚点后才能做幂等追加;锚点缺失或不唯一时必须在写入前停止。
136
141
 
137
142
  精确生成来源保留在 `.windy/generation.json`。当前版本已提供可回滚的
138
143
  `windy update` 与 `windy module list/enable/disable`。当前内置模块仍依赖
@@ -166,7 +171,8 @@ bun run format
166
171
  更新前无需提交、暂存或清理业务改动。`--dry-run` 只生成计划,不写项目;无关 WIP
167
172
  保持原样,命中同一路径时按冻结快照与三方合并 fail closed。正式更新先备份,再做
168
173
  package 字段级合并、共享脚手架三方合并与完整验证。整个 `apps/web/src` 和
169
- `apps/server/src/app` 保持项目版本。任何冲突都会在写入前中止,模块启停在交互
174
+ `apps/server/src/app` 默认保持项目版本;已登记版本 recipe 只修改其声明的窄锚点。
175
+ 任何冲突都会在写入前中止,模块启停在交互
170
176
  终端确认;CI、Agent 或其它非 TTY 环境必须显式传入 `--yes`。
171
177
  无并发变化的验证失败会自动回滚;中断恢复使用
172
178
  `bun run windy doctor --repair`。该命令在提交点前保留并发编辑并回滚其余路径,
@@ -135,6 +135,7 @@ class RuntimeRelease(CanonicalModel):
135
135
  workflow_version: str = Field(min_length=1, max_length=80)
136
136
  prompt_version: str = Field(min_length=1, max_length=80)
137
137
  route_version: str = Field(min_length=1, max_length=80)
138
+ output_schema_version: str = Field(min_length=1, max_length=80)
138
139
  tool_schema_version: str = Field(min_length=1, max_length=80)
139
140
  manifest_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
140
141
 
@@ -166,6 +167,7 @@ def legacy_release() -> RuntimeRelease:
166
167
  workflow_version="legacy",
167
168
  prompt_version="legacy",
168
169
  route_version="legacy",
170
+ output_schema_version="legacy",
169
171
  tool_schema_version="legacy",
170
172
  manifest_digest="0" * 64,
171
173
  )
@@ -49,3 +49,33 @@ def test_typed_image_part_and_json_schema_output() -> None:
49
49
  with pytest.raises(SafeEngineError) as invalid:
50
50
  final_output('{"name":42}', schema)
51
51
  assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
52
+
53
+
54
+ def test_supervisor_final_answer_schema_rejects_intermediate_text() -> None:
55
+ schema: dict[str, JsonValue] = {
56
+ "type": "object",
57
+ "properties": {
58
+ "finalAnswer": {
59
+ "type": "object",
60
+ "properties": {
61
+ "schemaVersion": {"const": 1},
62
+ "text": {"type": "string", "minLength": 1},
63
+ },
64
+ "required": ["schemaVersion", "text"],
65
+ "additionalProperties": False,
66
+ },
67
+ "nodes": {"type": "array"},
68
+ },
69
+ "required": ["finalAnswer"],
70
+ "additionalProperties": False,
71
+ }
72
+ assert final_output(
73
+ '{"finalAnswer":{"schemaVersion":1,"text":"最终回答"},"nodes":[]}',
74
+ schema,
75
+ ) == {
76
+ "finalAnswer": {"schemaVersion": 1, "text": "最终回答"},
77
+ "nodes": [],
78
+ }
79
+ with pytest.raises(SafeEngineError) as invalid:
80
+ final_output('{"nodes":[{"text":"中间节点","total":2}]}', schema)
81
+ assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
@@ -96,6 +96,7 @@ def fixture(suffix: str) -> RunRecord:
96
96
  "workflowVersion": "workflow-1",
97
97
  "promptVersion": "prompt-1",
98
98
  "routeVersion": "route-1",
99
+ "outputSchemaVersion": "output-schema-1",
99
100
  "toolSchemaVersion": "schema-1",
100
101
  "manifestDigest": "0" * 64,
101
102
  },
@@ -174,6 +174,7 @@ def fixture(run_id: str, idempotency_key: str) -> RunRecord:
174
174
  "workflowVersion": "workflow-1",
175
175
  "promptVersion": "prompt-1",
176
176
  "routeVersion": "route-1",
177
+ "outputSchemaVersion": "output-schema-1",
177
178
  "toolSchemaVersion": "schema-1",
178
179
  "manifestDigest": "0" * 64,
179
180
  },
@@ -210,6 +210,7 @@ function runtimeRelease(): AgentRuntimeOperation["release"] {
210
210
  workflowVersion: "workflow-1",
211
211
  promptVersion: "prompt-1",
212
212
  routeVersion: "route-1",
213
+ outputSchemaVersion: "output-schema-1",
213
214
  toolSchemaVersion: "schema-1",
214
215
  manifestDigest: "0".repeat(64),
215
216
  };
@@ -41,6 +41,7 @@ export function auditAgentRun(input: {
41
41
  workflowVersion: input.release.workflowVersion,
42
42
  promptVersion: input.release.promptVersion,
43
43
  routeVersion: input.release.routeVersion,
44
+ outputSchemaVersion: input.release.outputSchemaVersion,
44
45
  toolSchemaVersion: input.release.toolSchemaVersion,
45
46
  traceId: input.traceId,
46
47
  },
@@ -56,6 +56,7 @@ export interface AgentRuntimeRelease {
56
56
  readonly workflowVersion: string;
57
57
  readonly promptVersion: string;
58
58
  readonly routeVersion: string;
59
+ readonly outputSchemaVersion: string;
59
60
  readonly toolSchemaVersion: string;
60
61
  readonly manifestDigest: string;
61
62
  }
@@ -30,7 +30,6 @@ import {
30
30
  } from "./provider-input-governance.js";
31
31
  import {
32
32
  rotateVisionArtifacts,
33
- validateOutputSchema,
34
33
  validateVisionArtifacts,
35
34
  } from "./vision-input.js";
36
35
  import { requireVisionOperationReadiness } from "./vision-readiness.js";
@@ -55,6 +54,7 @@ import {
55
54
  buildRegisteredOperations,
56
55
  type RegisteredOperation,
57
56
  } from "./run-operation-registry.js";
57
+ import { validateRegisteredOutputSchema } from "./run-output-schema.js";
58
58
  export { AgentRunFacadeError } from "./run-errors.js";
59
59
 
60
60
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
@@ -157,7 +157,7 @@ export class AgentRunFacade {
157
157
  400,
158
158
  );
159
159
  }
160
- validateOutputSchema(plan.outputSchema, operation.definition);
160
+ validateRegisteredOutputSchema(plan.outputSchema, operation);
161
161
  const rotatedArtifacts = await rotateVisionArtifacts(
162
162
  artifacts,
163
163
  operation.definition,
@@ -13,6 +13,7 @@ export interface RegisteredOperation {
13
13
  feature: ReturnType<typeof findFeature>;
14
14
  tools: AgentRuntimeOperation["allowedTools"];
15
15
  release: AgentRuntimeOperation["release"];
16
+ outputSchema?: NonNullable<ModuleManifest["jsonSchemas"]>[number]["schema"];
16
17
  }
17
18
 
18
19
  export function buildRegisteredOperations(
@@ -26,6 +27,11 @@ export function buildRegisteredOperations(
26
27
  ]),
27
28
  );
28
29
  const features = modules.flatMap(({ features }) => features);
30
+ const schemas = new Map(
31
+ modules.flatMap(({ jsonSchemas }) =>
32
+ (jsonSchemas ?? []).map((schema) => [schema.key, schema.schema] as const),
33
+ ),
34
+ );
29
35
  return new Map(
30
36
  modules.flatMap((module) =>
31
37
  (module.aiOperations ?? []).map((definition) => {
@@ -37,6 +43,7 @@ export function buildRegisteredOperations(
37
43
  ...handler,
38
44
  feature: findFeature(features, definition.featureKey),
39
45
  tools: toolDefinitions(modules, definition.allowedToolKeys),
46
+ outputSchema: schemas.get(definition.outputSchemaKey),
40
47
  release: buildAgentRuntimeRelease(
41
48
  modules,
42
49
  module,
@@ -0,0 +1,15 @@
1
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
2
+ import type { RegisteredOperation } from "./run-operation-registry.js";
3
+ import { validateOutputSchema } from "./vision-input.js";
4
+
5
+ export function validateRegisteredOutputSchema(
6
+ schema: ModuleAiJsonValue | undefined,
7
+ operation: RegisteredOperation,
8
+ ): void {
9
+ validateOutputSchema(
10
+ schema,
11
+ operation.definition,
12
+ operation.registration?.supervisor,
13
+ operation.outputSchema,
14
+ );
15
+ }
@@ -20,6 +20,7 @@ export function buildAgentRuntimeRelease(
20
20
  const coordinates = {
21
21
  operationVersion: module.version,
22
22
  ...versions,
23
+ outputSchemaVersion: outputSchemaDigest(modules, definition),
23
24
  toolSchemaVersion: operationSchemaDigest(modules, definition),
24
25
  manifestDigest: digest(module),
25
26
  };
@@ -29,6 +30,16 @@ export function buildAgentRuntimeRelease(
29
30
  };
30
31
  }
31
32
 
33
+ function outputSchemaDigest(
34
+ modules: readonly ModuleManifest[],
35
+ definition: ModuleAiOperationDefinition,
36
+ ): string {
37
+ const schema = modules
38
+ .flatMap(({ jsonSchemas }) => jsonSchemas ?? [])
39
+ .find(({ key }) => key === definition.outputSchemaKey)?.schema;
40
+ return digest(schema);
41
+ }
42
+
32
43
  function operationSchemaDigest(
33
44
  modules: readonly ModuleManifest[],
34
45
  definition: ModuleAiOperationDefinition,
@@ -0,0 +1,95 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ composeSystemAgentFinalAnswerSchemaV1,
4
+ type ModuleAiOperationDefinition,
5
+ } from "@southwind-ai/modules";
6
+ import { validateOutputSchema } from "./vision-input.js";
7
+
8
+ const definition: ModuleAiOperationDefinition = {
9
+ key: "business.copilot",
10
+ kind: "agent",
11
+ featureKey: "business.copilot",
12
+ permissionKey: "business.copilot.execute",
13
+ auditAction: "business.copilot.execute",
14
+ inputSchemaKey: "business.copilot.input.v1",
15
+ outputSchemaKey: "business.copilot.output.v1",
16
+ requiredCapabilities: ["text-generation"],
17
+ allowedToolKeys: [],
18
+ dataClassification: "internal",
19
+ execution: "interactive",
20
+ providerEgress: { mode: "configured-endpoint", deploymentKey: "ai.default" },
21
+ providerDataPolicy: {
22
+ inputFields: [
23
+ {
24
+ jsonPointer: "/prompt",
25
+ classification: "internal",
26
+ dlpAction: "redact",
27
+ },
28
+ ],
29
+ envelopeFields: [
30
+ {
31
+ field: "instructions",
32
+ classification: "internal",
33
+ dlpAction: "redact",
34
+ },
35
+ {
36
+ field: "user-prompt",
37
+ classification: "internal",
38
+ dlpAction: "redact",
39
+ },
40
+ {
41
+ field: "output-schema",
42
+ classification: "internal",
43
+ dlpAction: "redact",
44
+ },
45
+ ],
46
+ },
47
+ toolNetworkPolicy: { mode: "forbidden" },
48
+ limits: {
49
+ maxDurationSeconds: 60,
50
+ maxInputBytes: 4_096,
51
+ maxOutputBytes: 1_048_576,
52
+ },
53
+ };
54
+
55
+ const supervisor = {
56
+ contractVersion: 1,
57
+ finalAnswerSchemaVersion: 1,
58
+ } as const;
59
+
60
+ describe("Supervisor output schema admission", () => {
61
+ test("只接受与 Manifest 完全一致的 canonical Schema", () => {
62
+ const schema = composeSystemAgentFinalAnswerSchemaV1({
63
+ properties: { citations: { type: "array" } },
64
+ });
65
+ expect(() =>
66
+ validateOutputSchema(schema, definition, supervisor, schema),
67
+ ).not.toThrow();
68
+ expect(() =>
69
+ validateOutputSchema(
70
+ composeSystemAgentFinalAnswerSchemaV1(),
71
+ definition,
72
+ supervisor,
73
+ schema,
74
+ ),
75
+ ).toThrow("与 Manifest 声明不一致");
76
+ });
77
+
78
+ test("拒绝缺失 Schema 或 legacy text Schema", () => {
79
+ expect(() =>
80
+ validateOutputSchema(undefined, definition, supervisor),
81
+ ).toThrow("必须提供 typed 输出 Schema");
82
+ expect(() =>
83
+ validateOutputSchema(
84
+ {
85
+ type: "object",
86
+ properties: { text: { type: "string" } },
87
+ required: ["text"],
88
+ additionalProperties: false,
89
+ },
90
+ definition,
91
+ supervisor,
92
+ ),
93
+ ).toThrow("canonical finalAnswer V1");
94
+ });
95
+ });
@@ -1,6 +1,10 @@
1
- import type { ModuleAiOperationDefinition } from "@southwind-ai/modules";
1
+ import {
2
+ assertSystemAgentFinalAnswerSchemaV1,
3
+ type ModuleAiOperationDefinition,
4
+ } from "@southwind-ai/modules";
2
5
  import type {
3
6
  ModuleAiArtifactRef,
7
+ ModuleAiOperationHandlerRegistration,
4
8
  ModuleAiJsonValue,
5
9
  } from "@southwind-ai/server-sdk";
6
10
  import type { AiArtifactRuntime } from "../ai-artifact/runtime.js";
@@ -56,8 +60,13 @@ export async function rotateVisionArtifacts(
56
60
  export function validateOutputSchema(
57
61
  schema: ModuleAiJsonValue | undefined,
58
62
  definition: ModuleAiOperationDefinition,
63
+ supervisor?: ModuleAiOperationHandlerRegistration["supervisor"],
64
+ declaredSchema?: ModuleAiJsonValue,
59
65
  ) {
60
- if (schema === undefined) return;
66
+ if (schema === undefined) {
67
+ if (supervisor) throw invalid("Supervisor 必须提供 typed 输出 Schema");
68
+ return;
69
+ }
61
70
  if (
62
71
  !schema ||
63
72
  typeof schema !== "object" ||
@@ -70,6 +79,16 @@ export function validateOutputSchema(
70
79
  400,
71
80
  );
72
81
  }
82
+ if (supervisor) {
83
+ try {
84
+ assertSystemAgentFinalAnswerSchemaV1(schema);
85
+ } catch {
86
+ throw invalid("Supervisor 输出 Schema 缺少 canonical finalAnswer V1");
87
+ }
88
+ if (canonicalJson(schema) !== canonicalJson(declaredSchema)) {
89
+ throw invalid("Supervisor 运行时输出 Schema 与 Manifest 声明不一致");
90
+ }
91
+ }
73
92
  if (
74
93
  definition.providerEgress?.mode === "configured-endpoint" &&
75
94
  !definition.providerDataPolicy?.envelopeFields.some(
@@ -84,6 +103,22 @@ export function validateOutputSchema(
84
103
  }
85
104
  }
86
105
 
106
+ function canonicalJson(value: unknown): string {
107
+ return JSON.stringify(canonicalValue(value));
108
+ }
109
+
110
+ function canonicalValue(value: unknown): unknown {
111
+ if (Array.isArray(value)) return value.map(canonicalValue);
112
+ if (value !== null && typeof value === "object") {
113
+ return Object.fromEntries(
114
+ Object.entries(value as Record<string, unknown>)
115
+ .sort(([left], [right]) => left.localeCompare(right))
116
+ .map(([key, item]) => [key, canonicalValue(item)]),
117
+ );
118
+ }
119
+ return value;
120
+ }
121
+
87
122
  export function validateVisionArtifacts(
88
123
  artifacts: readonly Pick<
89
124
  ModuleAiArtifactRef,
@@ -0,0 +1,138 @@
1
+ import { boundedJsonBodyErrorResponse } from "@southwind-ai/license";
2
+ import { LicensePairingError } from "./pairing-service.js";
3
+
4
+ export function createPairingCorsPolicy(
5
+ env: Record<string, string | undefined>,
6
+ ) {
7
+ const configured = env.WINDY_LICENSE_PAIRING_ORIGINS;
8
+ const candidates =
9
+ configured === undefined
10
+ ? env.NODE_ENV === "production"
11
+ ? []
12
+ : ["http://localhost:9843"]
13
+ : configured.split(",").map((value) => value.trim());
14
+ const origins = new Set(candidates.filter(isExactHttpOrigin));
15
+ return {
16
+ enabled: origins.size > 0,
17
+ accept: (request: Request) => {
18
+ const origin = request.headers.get("origin") || "";
19
+ return origins.has(origin) ? origin : undefined;
20
+ },
21
+ headers: (origin: string) => ({
22
+ "access-control-allow-origin": origin,
23
+ "access-control-allow-methods": "POST, OPTIONS",
24
+ "access-control-allow-headers": "Content-Type",
25
+ "access-control-allow-private-network": "true",
26
+ vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
27
+ }),
28
+ };
29
+ }
30
+
31
+ export function allowedPreflightHeaders(value: string | null): boolean {
32
+ if (!value) return true;
33
+ return value
34
+ .split(",")
35
+ .map((header) => header.trim().toLowerCase())
36
+ .every((header) => header === "content-type");
37
+ }
38
+
39
+ export function requireCallbackOrigin(
40
+ value: unknown,
41
+ request: Request,
42
+ ): string {
43
+ const callbackOrigin = requiredText(value, "callbackOrigin");
44
+ if (!isExactHttpOrigin(callbackOrigin)) {
45
+ throw new LicensePairingError(
46
+ "PAIRING_CALLBACK_ORIGIN_INVALID",
47
+ 400,
48
+ "callbackOrigin 必须是精确的 HTTP(S) Origin",
49
+ );
50
+ }
51
+ if (request.headers.get("origin") !== callbackOrigin) {
52
+ throw new LicensePairingError(
53
+ "PAIRING_CALLBACK_ORIGIN_MISMATCH",
54
+ 403,
55
+ "请求 Origin 与 callbackOrigin 不一致",
56
+ );
57
+ }
58
+ return callbackOrigin;
59
+ }
60
+
61
+ export function requireDeliveryContentType(request: Request): void {
62
+ const value = request.headers.get("content-type")?.toLowerCase() || "";
63
+ if (value.startsWith("application/json") || value.startsWith("text/plain")) {
64
+ return;
65
+ }
66
+ throw new LicensePairingError(
67
+ "PAIRING_CONTENT_TYPE_INVALID",
68
+ 415,
69
+ "投递只接受 application/json 或 text/plain JSON",
70
+ );
71
+ }
72
+
73
+ export function requiredText(value: unknown, name: string): string {
74
+ if (typeof value === "string" && value.trim()) return value;
75
+ throw new LicensePairingError(
76
+ "PAIRING_REQUEST_INVALID",
77
+ 400,
78
+ `${name} 必须为非空字符串`,
79
+ );
80
+ }
81
+
82
+ export function pairingErrorResponse(
83
+ error: unknown,
84
+ headers: Record<string, string> = {},
85
+ ): Response {
86
+ const bodyError = boundedJsonBodyErrorResponse(error);
87
+ if (bodyError) return noStore(bodyError, headers);
88
+ if (error instanceof LicensePairingError) {
89
+ return json(
90
+ { error: error.code, reason: error.message, ...error.details },
91
+ error.status,
92
+ headers,
93
+ );
94
+ }
95
+ throw error;
96
+ }
97
+
98
+ export function corsRejected(): Response {
99
+ return json({ error: "PAIRING_CORS_REJECTED" }, 403, { vary: "Origin" });
100
+ }
101
+
102
+ export function json(
103
+ body: unknown,
104
+ status = 200,
105
+ headers: Record<string, string> = {},
106
+ ): Response {
107
+ return Response.json(body, { status, headers: responseHeaders(headers) });
108
+ }
109
+
110
+ export function noStore(
111
+ response: Response,
112
+ headers: Record<string, string> = {},
113
+ ) {
114
+ const nextHeaders = new Headers(response.headers);
115
+ for (const [key, value] of Object.entries(responseHeaders(headers))) {
116
+ nextHeaders.set(key, value);
117
+ }
118
+ return new Response(response.body, {
119
+ status: response.status,
120
+ headers: nextHeaders,
121
+ });
122
+ }
123
+
124
+ export function responseHeaders(headers: Record<string, string>) {
125
+ return { ...headers, "cache-control": "no-store" };
126
+ }
127
+
128
+ function isExactHttpOrigin(value: string): boolean {
129
+ try {
130
+ const url = new URL(value);
131
+ return (
132
+ (url.protocol === "http:" || url.protocol === "https:") &&
133
+ url.origin === value
134
+ );
135
+ } catch {
136
+ return false;
137
+ }
138
+ }