@77-zhike/app-sdk 0.2.0-d7b1.2 → 0.2.0-d7b1.20

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 (96) hide show
  1. package/README.md +281 -0
  2. package/package.json +17 -3
  3. package/src/cli/api-client.js +4 -0
  4. package/src/cli/browser-auth.js +64 -18
  5. package/src/cli/credential-store.js +21 -0
  6. package/src/cli/development-logs.js +5 -2
  7. package/src/cli/index.js +59 -42
  8. package/src/cli/local-commands.js +6 -3
  9. package/src/cli/logs.js +56 -0
  10. package/src/cli/project-init.js +111 -179
  11. package/src/cli/windows-credential-bridge.ps1 +131 -0
  12. package/src/cli/windows-credential-store.js +112 -0
  13. package/src/client/analysis.d.ts +4 -0
  14. package/src/client/analysis.js +12 -0
  15. package/src/client/destinations.d.ts +25 -0
  16. package/src/client/destinations.js +70 -0
  17. package/src/client/documents.d.ts +21 -0
  18. package/src/client/documents.js +39 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +225 -3
  21. package/src/client/index.js +75 -3
  22. package/src/client/interactions.d.ts +23 -3
  23. package/src/client/interactions.js +53 -3
  24. package/src/client/points.generated.js +56 -0
  25. package/src/config.js +11 -7
  26. package/src/forms/array-contract.d.ts +32 -0
  27. package/src/forms/array-contract.js +138 -0
  28. package/src/forms/components.generated.js +2 -2
  29. package/src/forms/factory.js +21 -7
  30. package/src/forms/field-array.d.ts +16 -0
  31. package/src/forms/field-array.js +52 -0
  32. package/src/forms/index.d.ts +74 -3
  33. package/src/forms/index.js +2 -0
  34. package/src/forms/options.d.ts +7 -2
  35. package/src/forms/options.js +8 -1
  36. package/src/forms/record-options.d.ts +28 -0
  37. package/src/forms/record-options.js +92 -0
  38. package/src/forms/schema.d.ts +73 -9
  39. package/src/forms/schema.js +128 -0
  40. package/src/forms/state.d.ts +30 -0
  41. package/src/forms/state.js +82 -0
  42. package/src/forms/validation.d.ts +9 -0
  43. package/src/forms/validation.js +81 -0
  44. package/src/forms/values.d.ts +5 -0
  45. package/src/forms/values.js +38 -0
  46. package/src/jsx-dev-runtime.d.ts +2 -0
  47. package/src/jsx-dev-runtime.js +1 -0
  48. package/src/jsx-runtime.d.ts +2 -0
  49. package/src/jsx-runtime.js +1 -0
  50. package/src/server/agents/index.d.ts +62 -0
  51. package/src/server/agents/index.js +40 -0
  52. package/src/server/index.d.ts +60 -5
  53. package/src/server/index.js +43 -0
  54. package/src/server/steps/index.d.ts +71 -0
  55. package/src/server/steps/index.js +15 -0
  56. package/src/server/triggers/index.d.ts +68 -0
  57. package/src/server/triggers/index.js +27 -0
  58. package/src/server/workflow-blocks/index.d.ts +72 -0
  59. package/src/server/workflow-blocks/index.js +32 -0
  60. package/src/toolchain/agents/discovery.js +126 -0
  61. package/src/toolchain/agents/exposure.js +31 -0
  62. package/src/toolchain/agents/manifest.js +84 -0
  63. package/src/toolchain/ai-capabilities.js +69 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/dependency-policy.js +181 -0
  66. package/src/toolchain/index.js +0 -88
  67. package/src/toolchain/real-build.js +135 -27
  68. package/src/toolchain/source-discovery.js +330 -81
  69. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  70. package/src/toolchain/workflow-steps/manifest.js +99 -0
  71. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  72. package/src/ui/components.generated.js +4 -1
  73. package/src/ui/conversation.d.ts +81 -0
  74. package/src/ui/conversation.js +82 -0
  75. package/src/ui/data-table-model.d.ts +14 -0
  76. package/src/ui/data-table-model.js +98 -0
  77. package/src/ui/data-table.d.ts +70 -0
  78. package/src/ui/data-table.js +74 -0
  79. package/src/ui/events.d.ts +10 -0
  80. package/src/ui/factory.js +87 -3
  81. package/src/ui/index.d.ts +351 -8
  82. package/src/ui/index.js +1 -1
  83. package/src/ui/jsx.generated.d.ts +359 -0
  84. package/src/ui/native-dom.d.ts +49 -0
  85. package/src/ui/native-dom.js +44 -0
  86. package/src/ui/scroll-viewport.d.ts +22 -0
  87. package/src/ui/scroll-viewport.js +23 -0
  88. package/src/ui/slider.d.ts +27 -0
  89. package/src/ui/slider.js +26 -0
  90. package/src/ui/style.d.ts +5 -0
  91. package/src/ui/style.generated.js +2 -0
  92. package/src/ui/style.js +15 -0
  93. package/src/ui/text-selection.d.ts +22 -0
  94. package/src/ui/text-selection.js +23 -0
  95. package/src/ui/validation.js +86 -0
  96. package/src/cli/validate-bundle.js +0 -36
@@ -0,0 +1,131 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ $ProgressPreference = 'SilentlyContinue'
3
+ $PSModuleAutoLoadingPreference = 'None'
4
+
5
+ try {
6
+ if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
7
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_unavailable"}')
8
+ exit 0
9
+ }
10
+ Import-Module "$PSHOME\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1" -ErrorAction Stop
11
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
12
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
13
+ $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
14
+ $keys = @($request.PSObject.Properties.Name)
15
+ if ($request.version -ne 1 -or $request.operation -notin @('get', 'set', 'delete', 'probe') -or
16
+ @($keys | Where-Object { $_ -notin @('version', 'operation', 'origin', 'credential') }).Count -ne 0 -or
17
+ $request.origin -isnot [string] -or $request.origin.Length -gt 2048) { throw 'invalid request' }
18
+ $uri = New-Object System.Uri($request.origin, [System.UriKind]::Absolute)
19
+ if ($uri.Scheme -notin @('http', 'https') -or $uri.UserInfo -or $uri.AbsolutePath -ne '/' -or $uri.Query -or $uri.Fragment) { throw 'invalid origin' }
20
+ if ($request.operation -eq 'set') {
21
+ if ($request.credential -isnot [string] -or $request.credential.Length -lt 1 -or $request.credential.Length -gt 512) { throw 'invalid credential' }
22
+ } elseif ($keys -contains 'credential') { throw 'unexpected credential' }
23
+
24
+ # 仅绑定固定 Win32 API;全部在内存中构造,避免运行时编译器对 Unicode TEMP 的依赖。
25
+ $assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
26
+ (New-Object Reflection.AssemblyName('ZhikeCredentialBridge')), [Reflection.Emit.AssemblyBuilderAccess]::Run)
27
+ $module = $assembly.DefineDynamicModule('ZhikeCredentialBridge')
28
+ $layout = [Reflection.TypeAttributes]::Public -bor [Reflection.TypeAttributes]::SequentialLayout -bor [Reflection.TypeAttributes]::Sealed
29
+ $structure = $module.DefineType('ZhikeCredential', $layout, [ValueType])
30
+ # 字段顺序对应 CREDENTIALW;指针由当前进程位数决定,FILETIME 作为 64 位值对齐。
31
+ $fields = @(
32
+ @('Flags', [uint32]), @('Type', [uint32]), @('TargetName', [IntPtr]), @('Comment', [IntPtr]),
33
+ @('LastWritten', [long]), @('CredentialBlobSize', [uint32]), @('CredentialBlob', [IntPtr]),
34
+ @('Persist', [uint32]), @('AttributeCount', [uint32]), @('Attributes', [IntPtr]),
35
+ @('TargetAlias', [IntPtr]), @('UserName', [IntPtr]))
36
+ foreach ($field in $fields) { [void]$structure.DefineField($field[0], $field[1], [Reflection.FieldAttributes]::Public) }
37
+ $credentialType = $structure.CreateType()
38
+ $binding = $module.DefineType('ZhikeNativeCredential', [Reflection.TypeAttributes]::Public)
39
+ $dllImport = [Runtime.InteropServices.DllImportAttribute]
40
+ $importFields = [Reflection.FieldInfo[]]@($dllImport.GetField('EntryPoint'), $dllImport.GetField('CharSet'), $dllImport.GetField('SetLastError'))
41
+ $signatures = @(
42
+ @('Read', 'CredReadW', [bool], [type[]]@([string], [uint32], [uint32], [IntPtr].MakeByRefType())),
43
+ @('Write', 'CredWriteW', [bool], [type[]]@([IntPtr], [uint32])),
44
+ @('Delete', 'CredDeleteW', [bool], [type[]]@([string], [uint32], [uint32])),
45
+ @('Free', 'CredFree', [void], [type[]]@([IntPtr])))
46
+ foreach ($signature in $signatures) {
47
+ $method = $binding.DefineMethod($signature[0], [Reflection.MethodAttributes]'Public, Static, PinvokeImpl', $signature[2], $signature[3])
48
+ $attribute = New-Object Reflection.Emit.CustomAttributeBuilder(
49
+ $dllImport.GetConstructor([type[]]@([string])), [object[]]@('advapi32.dll'),
50
+ $importFields, [object[]]@($signature[1], [Runtime.InteropServices.CharSet]::Unicode, $true))
51
+ $method.SetCustomAttribute($attribute)
52
+ $method.SetImplementationFlags([Reflection.MethodImplAttributes]::PreserveSig)
53
+ }
54
+ $native = $binding.CreateType()
55
+ $hash = [Security.Cryptography.SHA256]::Create()
56
+ try { $digest = [BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($request.origin))).Replace('-', '').ToLowerInvariant() }
57
+ finally { $hash.Dispose() }
58
+ $target = 'com.77hub.zhike.developer-cli/v1/' + $digest
59
+ $value = $null
60
+ switch ($request.operation) {
61
+ { $_ -in @('get', 'probe') } {
62
+ $pointer = [IntPtr]::Zero
63
+ if (-not $native::Read($target, 1, 0, [ref]$pointer)) {
64
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
65
+ if ($errorCode -ne 1168) { throw 'credential read failed' }
66
+ } else {
67
+ $blob = [IntPtr]::Zero
68
+ $blobSize = 0
69
+ try {
70
+ # 按固定结构的实际偏移读取,避免 PowerShell 在 PtrToStructure 的 Type/Object 重载间歧义。
71
+ $kind = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Type').ToInt32())
72
+ $persist = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Persist').ToInt32())
73
+ $blobSize = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlobSize').ToInt32())
74
+ $blob = [Runtime.InteropServices.Marshal]::ReadIntPtr($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlob').ToInt32())
75
+ # Generic / 本用户本机跨登录持久化;BlobSize 单位为字节,内容为 UTF-16LE。
76
+ if ($kind -ne 1 -or $persist -ne 2 -or $blobSize -lt 1 -or $blobSize -gt 1024 -or
77
+ $blobSize % 2 -ne 0 -or $blob -eq [IntPtr]::Zero) { throw 'invalid credential metadata' }
78
+ if ($request.operation -eq 'get') {
79
+ $value = [Runtime.InteropServices.Marshal]::PtrToStringUni($blob, [int]($blobSize / 2))
80
+ }
81
+ } finally {
82
+ if ($blob -ne [IntPtr]::Zero -and $blobSize -gt 0 -and $blobSize -le 1024) {
83
+ for ($i = 0; $i -lt $blobSize; $i++) { [Runtime.InteropServices.Marshal]::WriteByte($blob, $i, 0) }
84
+ }
85
+ $native::Free($pointer)
86
+ }
87
+ }
88
+ }
89
+ 'set' {
90
+ $bytes = [Text.Encoding]::Unicode.GetBytes($request.credential)
91
+ $blob = [IntPtr]::Zero
92
+ $record = [IntPtr]::Zero
93
+ $namePointer = [IntPtr]::Zero
94
+ $userPointer = [IntPtr]::Zero
95
+ try {
96
+ $blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
97
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
98
+ $namePointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($target)
99
+ $userPointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($request.origin)
100
+ $credential = [Activator]::CreateInstance($credentialType)
101
+ $credential.Type = 1
102
+ $credential.TargetName = $namePointer
103
+ $credential.UserName = $userPointer
104
+ $credential.CredentialBlob = $blob
105
+ $credential.CredentialBlobSize = $bytes.Length
106
+ $credential.Persist = 2
107
+ $record = [Runtime.InteropServices.Marshal]::AllocHGlobal([Runtime.InteropServices.Marshal]::SizeOf($credential))
108
+ [Runtime.InteropServices.Marshal]::StructureToPtr($credential, $record, $false)
109
+ if (-not $native::Write($record, 0)) { throw 'credential write failed' }
110
+ } finally {
111
+ [Array]::Clear($bytes, 0, $bytes.Length)
112
+ if ($blob -ne [IntPtr]::Zero) {
113
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
114
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
115
+ }
116
+ foreach ($allocated in @($record, $namePointer, $userPointer)) {
117
+ if ($allocated -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeHGlobal($allocated) }
118
+ }
119
+ }
120
+ }
121
+ 'delete' {
122
+ if (-not $native::Delete($target, 1, 0)) {
123
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
124
+ if ($errorCode -ne 1168) { throw 'credential delete failed' }
125
+ }
126
+ }
127
+ }
128
+ [Console]::Out.Write((@{ version = 1; ok = $true; value = $value } | ConvertTo-Json -Compress))
129
+ } catch {
130
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_failed"}')
131
+ }
@@ -0,0 +1,112 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { win32 } from "node:path";
5
+
6
+ import { normalizeOrigin } from "../config.js";
7
+ import { ZhikeCliError } from "../errors.js";
8
+
9
+ let script;
10
+ const failure = () => new ZhikeCliError("zhike_secure_store_failed", "无法访问 Windows 安全凭据存储,请检查系统凭据库和 PowerShell 策略后重试");
11
+ const unavailable = () => new ZhikeCliError("zhike_secure_store_unavailable", "Windows 安全凭据适配不可用,需要系统 Windows PowerShell 5.1 及允许的原生 API 调用");
12
+
13
+ export class WindowsCredentialStore {
14
+ constructor({ runner = runWindowsCredentialBridge } = {}) {
15
+ this.runner = runner;
16
+ }
17
+
18
+ async get(environmentId) { return this.request("get", normalizeOrigin(environmentId, "Developer API")); }
19
+
20
+ async set(environmentId, refreshCredential) {
21
+ if (typeof refreshCredential !== "string" || !refreshCredential.length || refreshCredential.length > 512) throw failure();
22
+ await this.request("set", normalizeOrigin(environmentId, "Developer API"), refreshCredential);
23
+ }
24
+
25
+ async delete(environmentId) { await this.request("delete", normalizeOrigin(environmentId, "Developer API")); }
26
+
27
+ async probe() { await this.request("probe", `https://probe-${randomUUID()}.invalid`); }
28
+
29
+ async request(operation, origin, credential) {
30
+ try {
31
+ const result = await this.runner({ version: 1, operation, origin, ...(credential === undefined ? {} : { credential }) });
32
+ if (!result || result.version !== 1 || typeof result.ok !== "boolean" ||
33
+ Object.keys(result).some((key) => !["version", "ok", "value", "code"].includes(key))) throw failure();
34
+ if (!result.ok) {
35
+ if (result.code === "zhike_secure_store_unavailable") throw unavailable();
36
+ throw failure();
37
+ }
38
+ if (operation === "get") {
39
+ if (result.value !== null && (typeof result.value !== "string" || !result.value.length || result.value.length > 512)) throw failure();
40
+ return result.value;
41
+ }
42
+ if (result.value !== null) throw failure();
43
+ } catch (error) {
44
+ if (error instanceof ZhikeCliError) throw error;
45
+ throw failure();
46
+ }
47
+ }
48
+ }
49
+
50
+ /** 固定程序/脚本与秘密数据分离;子进程输出不得原文进入 CLI 错误。 */
51
+ export async function runWindowsCredentialBridge(request, {
52
+ spawnImpl = spawn, systemRoot = process.env.SystemRoot, environment = process.env,
53
+ timeoutMs = 10_000, maxOutputBytes = 65_536,
54
+ } = {}) {
55
+ if (!systemRoot || !win32.isAbsolute(systemRoot)) throw unavailable();
56
+ let source;
57
+ try { source = await (script ??= readFile(new URL("./windows-credential-bridge.ps1", import.meta.url), "utf8")); } catch { throw unavailable(); }
58
+ const executable = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
59
+ const encoded = Buffer.from(source, "utf16le").toString("base64");
60
+ const env = Object.fromEntries(Object.entries(environment).filter(([key]) =>
61
+ ["systemroot", "windir", "temp", "tmp", "path", "comspec", "systemdrive",
62
+ "userprofile", "appdata", "localappdata", "homedrive", "homepath", "username", "userdomain",
63
+ "programfiles", "programfiles(x86)", "programw6432", "programdata", "allusersprofile",
64
+ "pathext", "computername", "processor_architecture", "processor_identifier", "processor_level",
65
+ "processor_revision", "number_of_processors", "os"].includes(key.toLowerCase())));
66
+ // ConvertFrom-Json / Add-Type 的模块解析只使用系统目录,不加载项目或用户自定义模块。
67
+ // 固定子进程不需要持久化命令发现缓存;避免依赖 runner/用户的缓存路径或锁。
68
+ env.PSModuleAnalysisCachePath = "NUL";
69
+ env.PSModulePath = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules");
70
+ return new Promise((resolve, reject) => {
71
+ let child;
72
+ let output = "";
73
+ let outputSize = 0;
74
+ let settled = false;
75
+ let timer;
76
+ const finish = (error, result) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ output = "";
81
+ if (error) reject(error); else resolve(result);
82
+ };
83
+ const terminate = () => {
84
+ if (Number.isInteger(child?.pid) && process.platform === "win32") {
85
+ execFile(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"],
86
+ { windowsHide: true, timeout: 3_000 }, () => child.kill());
87
+ } else child?.kill();
88
+ };
89
+ try {
90
+ child = spawnImpl(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded],
91
+ { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env });
92
+ } catch { finish(unavailable()); return; }
93
+ timer = setTimeout(() => { terminate(); finish(failure()); }, timeoutMs);
94
+ const collect = (bytes, keep) => {
95
+ if (settled) return;
96
+ outputSize += Buffer.byteLength(bytes);
97
+ if (outputSize > maxOutputBytes) { terminate(); finish(failure()); return; }
98
+ if (keep) output += bytes;
99
+ };
100
+ child.stdout.setEncoding("utf8");
101
+ child.stdout.on("data", (bytes) => collect(bytes, true));
102
+ child.stderr.on("data", (bytes) => collect(bytes, false));
103
+ child.on("error", () => finish(unavailable()));
104
+ child.stdin.on("error", () => { terminate(); finish(failure()); });
105
+ child.on("close", (code) => {
106
+ if (settled) return;
107
+ if (code !== 0) { finish(failure()); return; }
108
+ try { finish(null, JSON.parse(output)); } catch { finish(failure()); }
109
+ });
110
+ child.stdin.end(JSON.stringify(request), "utf8");
111
+ });
112
+ }
@@ -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,21 @@
1
+ import type { JsonValue } from '../ui/index.js';
2
+ /** 受限JSON Schema;服务端校验完整白名单、数组/字符串上限与绑定。 */
3
+ export type RestrictedInputSchema = Readonly<Record<string, JsonValue>>;
4
+ export interface PreparePdfTemplateInput {
5
+ inputSchema: RestrictedInputSchema;
6
+ /** 本次制作的有界、已授权试填数据,不保存到App模板配置。 */
7
+ sampleData: Readonly<Record<string, JsonValue>>;
8
+ }
9
+ export interface OpenPdfDocumentInput {
10
+ /** opaque Job用途引用;不能传URL、附件ID或PDF字节。 */
11
+ documentRef: string;
12
+ action: 'open' | 'download';
13
+ }
14
+ export type PreparedPdfTemplate = Readonly<{templateFileRef: string}>;
15
+ export type OpenPdfDocumentResult = Readonly<{status: 'opened' | 'started' | 'cancelled'}>;
16
+ export function assertDocumentInput(operation: string, input: unknown): PreparePdfTemplateInput | OpenPdfDocumentInput;
17
+ export function assertDocumentResult(operation: string, value: unknown): PreparedPdfTemplate | OpenPdfDocumentResult | null;
18
+ export function createDocumentClient(invoke: (operation: string, input: unknown) => Promise<unknown>): {
19
+ preparePdfTemplate(input: PreparePdfTemplateInput): Promise<PreparedPdfTemplate | null>;
20
+ openPdfDocument(input: OpenPdfDocumentInput): Promise<OpenPdfDocumentResult>;
21
+ };
@@ -0,0 +1,39 @@
1
+ const invalid = () => { throw new Error('runtime_document_input_invalid'); };
2
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
3
+ const ref = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/.test(value);
4
+ function bounded(value, depth = 0) {
5
+ if (depth > 12) invalid();
6
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
7
+ if (typeof value === 'number' && Number.isFinite(value)) return;
8
+ if (Array.isArray(value)) { if (value.length > 1000) invalid(); for (const item of value) bounded(item, depth + 1); return; }
9
+ if (!object(value)) invalid();
10
+ for (const [key, item] of Object.entries(value)) {
11
+ if (['__proto__', 'prototype', 'constructor'].includes(key)) invalid();
12
+ bounded(item, depth + 1);
13
+ }
14
+ }
15
+ export function assertDocumentInput(operation, input) {
16
+ if (!object(input)) invalid();
17
+ if (operation === 'openPdfDocument') {
18
+ if (Object.keys(input).some(key => !['documentRef', 'action'].includes(key)) || !ref(input.documentRef)
19
+ || !['open', 'download'].includes(input.action)) invalid();
20
+ } else if (operation === 'preparePdfTemplate') {
21
+ if (Object.keys(input).some(key => !['inputSchema', 'sampleData'].includes(key)) || !object(input.inputSchema)
22
+ || input.inputSchema.type !== 'object' || input.inputSchema.additionalProperties !== false || !object(input.sampleData)) invalid();
23
+ bounded(input);
24
+ if (new TextEncoder().encode(JSON.stringify(input)).length > 512 * 1024) invalid();
25
+ } else invalid();
26
+ // 快照避免App在异步交互期间原位改动请求。
27
+ return JSON.parse(JSON.stringify(input));
28
+ }
29
+ export function assertDocumentResult(operation, value) {
30
+ if (operation === 'preparePdfTemplate' && value === null) return null;
31
+ if (!object(value) || Object.keys(value).length !== 1) invalid();
32
+ if (operation === 'preparePdfTemplate' ? !ref(value.templateFileRef) : !['opened', 'started', 'cancelled'].includes(value.status)) invalid();
33
+ return Object.freeze({ ...value });
34
+ }
35
+ export function createDocumentClient(invoke) {
36
+ return Object.freeze(Object.fromEntries(['preparePdfTemplate', 'openPdfDocument'].map(operation => [operation,
37
+ input => invoke(operation, assertDocumentInput(operation, input)).then(value => assertDocumentResult(operation, value)),
38
+ ])));
39
+ }
@@ -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
+ }