@77-zhike/app-sdk 0.2.0-d7b1.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 (87) hide show
  1. package/README.md +251 -0
  2. package/package.json +71 -0
  3. package/src/bin.js +15 -0
  4. package/src/cli/api-client.js +249 -0
  5. package/src/cli/browser-auth.js +212 -0
  6. package/src/cli/credential-store.js +101 -0
  7. package/src/cli/development-app.js +77 -0
  8. package/src/cli/development-logs.js +183 -0
  9. package/src/cli/development-watch.js +205 -0
  10. package/src/cli/index.js +256 -0
  11. package/src/cli/local-commands.js +80 -0
  12. package/src/cli/logs.js +56 -0
  13. package/src/cli/project-init.js +259 -0
  14. package/src/cli/upload-bundle.js +73 -0
  15. package/src/client/analysis.d.ts +4 -0
  16. package/src/client/analysis.js +12 -0
  17. package/src/client/destinations.d.ts +25 -0
  18. package/src/client/destinations.js +70 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +249 -0
  21. package/src/client/index.js +72 -0
  22. package/src/client/interactions.d.ts +23 -0
  23. package/src/client/interactions.js +62 -0
  24. package/src/client/points.generated.js +67 -0
  25. package/src/config.js +68 -0
  26. package/src/development-workspace.js +154 -0
  27. package/src/errors.js +20 -0
  28. package/src/forms/array-contract.d.ts +32 -0
  29. package/src/forms/array-contract.js +138 -0
  30. package/src/forms/components.generated.js +4 -0
  31. package/src/forms/factory.js +41 -0
  32. package/src/forms/field-array.d.ts +16 -0
  33. package/src/forms/field-array.js +52 -0
  34. package/src/forms/index.d.ts +118 -0
  35. package/src/forms/index.js +5 -0
  36. package/src/forms/normalize-options.js +19 -0
  37. package/src/forms/options.d.ts +30 -0
  38. package/src/forms/options.js +55 -0
  39. package/src/forms/record-options.d.ts +28 -0
  40. package/src/forms/record-options.js +92 -0
  41. package/src/forms/schema.d.ts +82 -0
  42. package/src/forms/schema.js +128 -0
  43. package/src/forms/state.d.ts +30 -0
  44. package/src/forms/state.js +82 -0
  45. package/src/forms/validation.d.ts +9 -0
  46. package/src/forms/validation.js +81 -0
  47. package/src/forms/values.d.ts +5 -0
  48. package/src/forms/values.js +38 -0
  49. package/src/index.d.ts +13 -0
  50. package/src/index.js +2 -0
  51. package/src/jsx-dev-runtime.d.ts +2 -0
  52. package/src/jsx-dev-runtime.js +1 -0
  53. package/src/jsx-runtime.d.ts +2 -0
  54. package/src/jsx-runtime.js +1 -0
  55. package/src/project-binding.js +84 -0
  56. package/src/server/index.d.ts +318 -0
  57. package/src/server/index.js +242 -0
  58. package/src/server/steps/index.d.ts +71 -0
  59. package/src/server/steps/index.js +15 -0
  60. package/src/server/triggers/index.d.ts +68 -0
  61. package/src/server/triggers/index.js +27 -0
  62. package/src/server/workflow-blocks/index.d.ts +72 -0
  63. package/src/server/workflow-blocks/index.js +32 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/canonical-json.js +17 -0
  66. package/src/toolchain/dependency-policy.js +181 -0
  67. package/src/toolchain/index.js +15 -0
  68. package/src/toolchain/real-build.js +459 -0
  69. package/src/toolchain/source-discovery.js +919 -0
  70. package/src/toolchain/transport-tar.js +168 -0
  71. package/src/toolchain/webhook-response.js +50 -0
  72. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  73. package/src/toolchain/workflow-steps/manifest.js +99 -0
  74. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  75. package/src/ui/components.generated.js +5 -0
  76. package/src/ui/events.d.ts +10 -0
  77. package/src/ui/factory.js +70 -0
  78. package/src/ui/icons.generated.js +2 -0
  79. package/src/ui/index.d.ts +343 -0
  80. package/src/ui/index.js +4 -0
  81. package/src/ui/jsx.generated.d.ts +359 -0
  82. package/src/ui/native-dom.d.ts +49 -0
  83. package/src/ui/native-dom.js +44 -0
  84. package/src/ui/style.d.ts +5 -0
  85. package/src/ui/style.generated.js +2 -0
  86. package/src/ui/style.js +15 -0
  87. package/src/ui/validation.js +86 -0
@@ -0,0 +1,259 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import {
3
+ access,
4
+ mkdir,
5
+ readFile,
6
+ rename,
7
+ rm,
8
+ stat,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { constants } from "node:fs";
12
+ import { basename, dirname, resolve } from "node:path";
13
+
14
+ import { normalizeOrigin, TOOLING_VERSION } from "../config.js";
15
+ import { ZhikeCliError } from "../errors.js";
16
+ import { serializeProjectBinding } from "../project-binding.js";
17
+
18
+ export async function initializeProject({
19
+ targetDirectory,
20
+ appSlug,
21
+ apiBaseUrl,
22
+ authenticatedApi,
23
+ scaffold = scaffoldProject,
24
+ }) {
25
+ validateAppSlug(appSlug);
26
+ const target = resolve(targetDirectory);
27
+ await assertEmptyTarget(target);
28
+
29
+ const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi });
30
+ await scaffold({ targetDirectory: target, binding });
31
+ return { app, organization, binding, created, targetDirectory: target };
32
+ }
33
+
34
+ export async function initializeExistingProject({ projectDirectory, appSlug, apiBaseUrl, authenticatedApi }) {
35
+ validateAppSlug(appSlug);
36
+ const target = resolve(projectDirectory);
37
+ await assertExistingProjectTarget(target);
38
+ const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi });
39
+ await mkdir(resolve(target, ".zhike"), { recursive: true });
40
+ await writeFile(resolve(target, ".zhike", "project.json"), serializeProjectBinding(binding), { flag: "wx" });
41
+ return { app, organization, binding, created, targetDirectory: target };
42
+ }
43
+
44
+ export async function assertExistingProjectTarget(projectDirectory) {
45
+ let projectPackage;
46
+ try {
47
+ projectPackage = JSON.parse(await readFile(resolve(projectDirectory, "package.json"), "utf8"));
48
+ const source = await stat(resolve(projectDirectory, "src"));
49
+ if (!source.isDirectory() || (!projectPackage?.devDependencies?.["@77-zhike/app-sdk"] &&
50
+ !projectPackage?.dependencies?.["@77-zhike/app-sdk"])) {
51
+ throw new Error("not an App project");
52
+ }
53
+ } catch {
54
+ throw new ZhikeCliError("zhike_existing_project_invalid", "当前目录不是包含 SDK 和 src 的 App 项目");
55
+ }
56
+ try {
57
+ await stat(resolve(projectDirectory, ".zhike", "project.json"));
58
+ throw new ZhikeCliError("zhike_project_already_bound", "当前项目已有 App 绑定,不能重复初始化");
59
+ } catch (error) {
60
+ if (error instanceof ZhikeCliError) throw error;
61
+ if (error?.code !== "ENOENT") {
62
+ throw new ZhikeCliError("zhike_project_binding_check_failed", "无法检查当前项目的 App 绑定", { cause: error });
63
+ }
64
+ }
65
+ }
66
+
67
+ async function resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi }) {
68
+ const organizations = await authenticatedApi.listOrganizations();
69
+ if (!Array.isArray(organizations) || organizations.length !== 1) {
70
+ throw new ZhikeCliError(
71
+ "zhike_developer_organization_unavailable",
72
+ organizations?.length === 0
73
+ ? "请先在 Developer Console 激活开发者组织"
74
+ : "当前账号存在多个开发者组织,D2-2 暂不支持隐式选择",
75
+ );
76
+ }
77
+ const organization = organizations[0];
78
+ let app = await authenticatedApi.getAppBySlug(organization.id, appSlug);
79
+ let created = false;
80
+ if (!app) {
81
+ app = await authenticatedApi.createApp(organization.id, {
82
+ name: displayNameFromSlug(appSlug),
83
+ slug: appSlug,
84
+ idempotencyKey: createInitIdempotencyKey({
85
+ apiBaseUrl,
86
+ organizationId: organization.id,
87
+ appSlug,
88
+ }),
89
+ });
90
+ created = true;
91
+ }
92
+ const binding = {
93
+ schemaVersion: 1,
94
+ platformEnvironment: apiBaseUrl,
95
+ appId: app.id,
96
+ organizationId: organization.id,
97
+ appSlug: app.slug,
98
+ };
99
+ return { app, organization, binding, created };
100
+ }
101
+
102
+ export async function assertEmptyTarget(targetDirectory) {
103
+ try {
104
+ await stat(targetDirectory);
105
+ throw new ZhikeCliError(
106
+ "zhike_target_exists",
107
+ `目标路径已存在:${basename(targetDirectory)}`,
108
+ );
109
+ } catch (error) {
110
+ if (error instanceof ZhikeCliError) {
111
+ throw error;
112
+ }
113
+ if (error?.code !== "ENOENT") {
114
+ throw new ZhikeCliError(
115
+ "zhike_target_check_failed",
116
+ "无法检查项目目标路径",
117
+ { cause: error },
118
+ );
119
+ }
120
+ }
121
+ try {
122
+ await access(dirname(targetDirectory), constants.W_OK);
123
+ } catch (error) {
124
+ throw new ZhikeCliError(
125
+ "zhike_target_parent_not_writable",
126
+ "项目目标路径的父目录不可写",
127
+ { cause: error },
128
+ );
129
+ }
130
+ }
131
+
132
+ export async function scaffoldProject({ targetDirectory, binding }) {
133
+ const docsUrl = `${normalizeOrigin(binding.platformEnvironment, "platformEnvironment")}/docs/`;
134
+ const agentInstructions = `# 智客 App 开发
135
+
136
+ 开始开发或修改此 App 前,先读取当前平台环境的开发文档:
137
+
138
+ - 文档索引:${docsUrl}llms.txt
139
+ - 开发文档:${docsUrl}
140
+
141
+ 先读取文档索引,再按当前任务读取相关的 App SDK、CLI、扩展、连接和 API 文档;不要仅凭记忆猜测接口、参数或运行时能力。
142
+ 每使用一个复合 UI 或 Form 组件,必须打开该组件的专属文档,核对允许的直属子节点、嵌套顺序和入口限制。TypeScript、zhike validate、zhike build 或上传成功,不代表组件树已经符合运行时合同。
143
+ 索引中的相对链接以开发文档地址为基准解析。若无法访问文档,请明确说明访问失败,请用户提供相关文档,不要把猜测当作已确认能力。
144
+ 项目绑定环境与 App 身份见 .zhike/project.json;切换平台环境后应同步更新上述文档地址。
145
+ `;
146
+ const temporaryDirectory = `${targetDirectory}.zhike-tmp-${randomBytes(6).toString("hex")}`;
147
+ try {
148
+ await mkdir(resolve(temporaryDirectory, ".zhike"), { recursive: true });
149
+ await mkdir(
150
+ resolve(temporaryDirectory, "src", "app", "extensions"),
151
+ { recursive: true },
152
+ );
153
+ await mkdir(resolve(temporaryDirectory, "src", "app", "webhooks"), {
154
+ recursive: true,
155
+ });
156
+ await mkdir(resolve(temporaryDirectory, "src", "app", "events"), {
157
+ recursive: true,
158
+ });
159
+ await mkdir(resolve(temporaryDirectory, "src", "server", "functions"), {
160
+ recursive: true,
161
+ });
162
+ for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
163
+ await writeFile(resolve(temporaryDirectory, filename), agentInstructions, { flag: "wx" });
164
+ }
165
+ await writeFile(
166
+ resolve(temporaryDirectory, ".zhike", "project.json"),
167
+ serializeProjectBinding(binding),
168
+ { flag: "wx" },
169
+ );
170
+ await writeFile(
171
+ resolve(temporaryDirectory, "package.json"),
172
+ `${JSON.stringify(
173
+ {
174
+ name: binding.appSlug,
175
+ private: true,
176
+ version: "0.0.0",
177
+ type: "module",
178
+ scripts: {
179
+ zhike: "zhike",
180
+ validate: "zhike validate",
181
+ build: "zhike build",
182
+ dev: "zhike dev",
183
+ upload: "zhike upload",
184
+ },
185
+ devDependencies: {
186
+ "@77-zhike/app-sdk": TOOLING_VERSION,
187
+ },
188
+ },
189
+ null,
190
+ 2,
191
+ )}\n`,
192
+ { flag: "wx" },
193
+ );
194
+ await writeFile(
195
+ resolve(temporaryDirectory, "tsconfig.json"),
196
+ `${JSON.stringify(
197
+ {
198
+ compilerOptions: {
199
+ lib: ["ES2022", "DOM"],
200
+ jsx: "react-jsx",
201
+ jsxImportSource: "@77-zhike/app-sdk",
202
+ module: "ESNext",
203
+ moduleResolution: "Bundler",
204
+ noEmit: true,
205
+ strict: true,
206
+ target: "ES2022",
207
+ },
208
+ include: ["src"],
209
+ },
210
+ null,
211
+ 2,
212
+ )}\n`,
213
+ { flag: "wx" },
214
+ );
215
+ await writeFile(
216
+ resolve(temporaryDirectory, ".gitignore"),
217
+ "dist/\n.zhike/development.json\n",
218
+ { flag: "wx" },
219
+ );
220
+ await rename(temporaryDirectory, targetDirectory);
221
+ } catch (error) {
222
+ await rm(temporaryDirectory, { recursive: true, force: true });
223
+ if (error instanceof ZhikeCliError) {
224
+ throw error;
225
+ }
226
+ throw new ZhikeCliError(
227
+ "zhike_scaffold_failed",
228
+ "App 已确认,但本地项目生成失败;修复文件系统问题后可安全重试",
229
+ { cause: error, retryable: true },
230
+ );
231
+ }
232
+ }
233
+
234
+ export function createInitIdempotencyKey({
235
+ apiBaseUrl,
236
+ organizationId,
237
+ appSlug,
238
+ }) {
239
+ return `init-${createHash("sha256")
240
+ .update(`${apiBaseUrl}\n${organizationId}\n${appSlug}`, "utf8")
241
+ .digest("hex")}`;
242
+ }
243
+
244
+ export function displayNameFromSlug(slug) {
245
+ return slug
246
+ .split("-")
247
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
248
+ .join(" ");
249
+ }
250
+
251
+ export function validateAppSlug(value) {
252
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value || "") || value.length > 100) {
253
+ throw new ZhikeCliError(
254
+ "zhike_app_slug_invalid",
255
+ "App slug 只能包含小写字母、数字和中划线",
256
+ );
257
+ }
258
+ return value;
259
+ }
@@ -0,0 +1,73 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { ZhikeCliError } from "../errors.js";
4
+ import { readProjectBinding } from "../project-binding.js";
5
+ import { buildAppBundle } from "../toolchain/real-build.js";
6
+
7
+ export async function uploadAppBundle({
8
+ projectDirectory,
9
+ apiBaseUrl,
10
+ authenticatedApi,
11
+ maxAttempts = 2,
12
+ expectedScopeRevision,
13
+ }) {
14
+ const bundle = await buildAppBundle({
15
+ projectDirectory,
16
+ expectedEnvironment: apiBaseUrl,
17
+ writeToDist: true,
18
+ development: true,
19
+ });
20
+ const binding = await readProjectBinding(projectDirectory, apiBaseUrl);
21
+ const idempotencyKey = `bundle-${randomUUID()}`;
22
+ let attempt = 0;
23
+ let upload = null;
24
+ while (attempt < maxAttempts) {
25
+ attempt += 1;
26
+ try {
27
+ upload = await authenticatedApi.uploadBundle(
28
+ binding.appId,
29
+ bundle,
30
+ idempotencyKey,
31
+ );
32
+ break;
33
+ } catch (error) {
34
+ if (!error?.retryable || attempt >= maxAttempts) throw error;
35
+ }
36
+ }
37
+ if (!upload || upload.status !== "accepted" || !upload.app_bundle_id) {
38
+ throw new ZhikeCliError(
39
+ "zhike_bundle_acceptance_invalid",
40
+ "Developer Server 未返回可激活的 App Bundle",
41
+ );
42
+ }
43
+
44
+ let developmentManifest = null;
45
+ let manifestAttempts = 0;
46
+ while (manifestAttempts < maxAttempts) {
47
+ manifestAttempts += 1;
48
+ try {
49
+ developmentManifest = await authenticatedApi.updateDevelopmentManifest(
50
+ binding.appId, upload.app_bundle_id, expectedScopeRevision,
51
+ );
52
+ break;
53
+ } catch (error) {
54
+ if (error?.retryable && manifestAttempts < maxAttempts) continue;
55
+ const partial = new ZhikeCliError(
56
+ "zhike_bundle_accepted_manifest_update_failed",
57
+ `Bundle ${upload.id} 已受理,但当前 Development Manifest 更新失败:${error.message}`,
58
+ { cause: error, retryable: error.retryable ?? false },
59
+ );
60
+ partial.acceptedUploadId = upload.id;
61
+ partial.acceptedAppBundleId = upload.app_bundle_id;
62
+ throw partial;
63
+ }
64
+ }
65
+ return {
66
+ artifact: bundle,
67
+ binding,
68
+ upload,
69
+ developmentManifest,
70
+ attempts: attempt,
71
+ manifestAttempts,
72
+ };
73
+ }
@@ -0,0 +1,4 @@
1
+ import type { JsonValue } from '../ui/index.js';
2
+ export interface AnalysisRequest { operation: 'list' | 'get' | 'execute'; input: { [key: string]: JsonValue } }
3
+ export function assertAnalysisRequest(value: unknown): AnalysisRequest;
4
+ export function createAnalysisClient(invoke: (operation: string, input: AnalysisRequest) => Promise<JsonValue>): (request: AnalysisRequest) => Promise<JsonValue>;
@@ -0,0 +1,12 @@
1
+ import { assertJsonValue } from '../ui/validation.js';
2
+
3
+ /** 当前成员的受控只读分析;不接受身份、URL 或任意执行代码。 */
4
+ export function assertAnalysisRequest(request) {
5
+ assertJsonValue(request, { maxBytes: 64 * 1024, maxEntries: 4000 });
6
+ if (!request || Array.isArray(request) || typeof request !== 'object' || Object.keys(request).some(key => !['operation', 'input'].includes(key))
7
+ || !['list', 'get', 'execute'].includes(request.operation) || !request.input || Array.isArray(request.input) || typeof request.input !== 'object') throw new Error('runtime_analysis_request_invalid');
8
+ return request;
9
+ }
10
+ export function createAnalysisClient(invoke) {
11
+ return request => invoke('analysis', assertAnalysisRequest(request));
12
+ }
@@ -0,0 +1,25 @@
1
+ /** 有界字符串业务参数;不包含宿主身份,不授予记录访问权。 */
2
+ export type PageParams = Readonly<Record<string, string>>;
3
+
4
+ export interface AppPageDestination {
5
+ readonly kind: 'app-page';
6
+ /** 当前 App 内的 Page slug。 */
7
+ readonly pageSlug: string;
8
+ readonly params?: PageParams;
9
+ }
10
+
11
+ export interface RecordDestination {
12
+ readonly kind: 'record';
13
+ /** 标准对象复数 slug,或当前 Workspace 可见的 custom_* 公开 API slug。 */
14
+ readonly object: string;
15
+ readonly recordId: string;
16
+ }
17
+
18
+ export type AppDestination = AppPageDestination | RecordDestination;
19
+
20
+ export function assertAppDestination(value: unknown): Readonly<AppDestination>;
21
+ export function assertPageParams(value: unknown): PageParams;
22
+ export const Destinations: Readonly<{
23
+ appPage(pageSlug: string, options?: { params?: PageParams }): Readonly<AppPageDestination>;
24
+ record(input: { object: string; recordId: string }): Readonly<RecordDestination>;
25
+ }>;
@@ -0,0 +1,70 @@
1
+ const PAGE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
+ const OBJECT_SLUG = /^[a-z][a-z0-9_]{0,99}$/;
3
+ const STANDARD_RECORD_OBJECTS = new Set([
4
+ 'accounts',
5
+ 'contacts',
6
+ 'opportunities',
7
+ 'tasks',
8
+ 'activities',
9
+ ]);
10
+
11
+ const fail = () => { throw new TypeError('runtime_destination_invalid'); };
12
+ const only = (value, keys) => {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value)
14
+ || Object.keys(value).some(key => !keys.includes(key))) fail();
15
+ };
16
+
17
+ /** 参数是地址中的不可信业务输入,不承载宿主身份或授权。 */
18
+ export function assertPageParams(value) {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)
20
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail();
21
+ const keys = Object.keys(value).sort();
22
+ if (keys.length > 16) fail();
23
+ const entries = keys.map(key => {
24
+ const item = value[key];
25
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key)
26
+ || ['__proto__', 'prototype', 'constructor'].includes(key)
27
+ || typeof item !== 'string' || [...item].length > 512
28
+ || /[\p{Cc}\p{Cs}]/u.test(item)) fail();
29
+ return [key, item];
30
+ });
31
+ const params = Object.fromEntries(entries);
32
+ if (new TextEncoder().encode(JSON.stringify(params)).byteLength > 2048) fail();
33
+ return Object.freeze(params);
34
+ }
35
+
36
+ export function assertAppDestination(value) {
37
+ only(value, value?.kind === 'app-page' ? ['kind', 'pageSlug', 'params'] : value?.kind === 'record'
38
+ ? ['kind', 'object', 'recordId'] : []);
39
+ if (value.kind === 'app-page') {
40
+ if (typeof value.pageSlug !== 'string' || value.pageSlug.length > 64 || !PAGE_SLUG.test(value.pageSlug)) fail();
41
+ const params = value.params === undefined ? undefined : assertPageParams(value.params);
42
+ return Object.freeze({ kind: 'app-page', pageSlug: value.pageSlug,
43
+ ...(params && Object.keys(params).length ? { params } : {}),
44
+ });
45
+ }
46
+ if (value.kind === 'record') {
47
+ const custom = typeof value.object === 'string' && value.object.startsWith('custom_');
48
+ if (
49
+ typeof value.object !== 'string'
50
+ || !OBJECT_SLUG.test(value.object)
51
+ || (!STANDARD_RECORD_OBJECTS.has(value.object) && !custom)
52
+ || typeof value.recordId !== 'string'
53
+ || value.recordId.length < 1
54
+ || value.recordId.length > 160
55
+ ) fail();
56
+ return Object.freeze({ kind: 'record', object: value.object, recordId: value.recordId });
57
+ }
58
+ fail();
59
+ }
60
+
61
+ export const Destinations = Object.freeze({
62
+ appPage(pageSlug, options) {
63
+ if (options !== undefined) only(options, ['params']);
64
+ return assertAppDestination({ kind: 'app-page', pageSlug, ...(options?.params === undefined ? {} : { params: options.params }) });
65
+ },
66
+ record(input) {
67
+ only(input, ['object', 'recordId']);
68
+ return assertAppDestination({ kind: 'record', object: input.object, recordId: input.recordId });
69
+ },
70
+ });
@@ -0,0 +1,9 @@
1
+ /** SDK 与 Host 重复验证同一有界 HTTPS 合同,不接受浏览器自动纠正的危险写法。 */
2
+ export function assertExternalUrl(value) {
3
+ if (typeof value !== 'string' || !value.length || value.length > 2048 || value !== value.trim()
4
+ || /[\p{Cc}\\]/u.test(value) || !/^https:\/\//i.test(value)) throw new Error('runtime_external_url_invalid');
5
+ let url;
6
+ try { url = new URL(value); } catch { throw new Error('runtime_external_url_invalid'); }
7
+ if (url.protocol !== 'https:' || !url.hostname || url.username || url.password) throw new Error('runtime_external_url_invalid');
8
+ return url.href;
9
+ }