@webskill/sdk 0.1.5 → 0.2.1

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/README.md CHANGED
@@ -15,12 +15,16 @@ governance.
15
15
  the LLM, guardrails, lifecycle events, memory, human interaction
16
16
  (missing-parameter forms, confirmations, authorization prompts), and
17
17
  resumable interrupted runs.
18
- - **Script sandbox** — `worker_threads` sandbox for Node, Web Worker sandbox for
19
- the browser. Deny-by-default network policy, deny-by-default builtin-module
20
- allowlist, per-capability forced approval, timeouts kill runaway scripts.
21
- **Honest note:** these executors reduce the capability surface they are NOT
22
- a security boundary. Scripts share the host process/origin and bypasses
23
- exist; never rely on them to isolate untrusted scripts.
18
+ - **Script sandbox** — two tiers. Isolation-grade: Node `ProcessSandboxExecutor`
19
+ (`child_process.fork` + `--permission`, experimental; fs scoped to the skill
20
+ root, workers/child processes/addons denied by default note the permission
21
+ model has NO network dimension, so `fetch`/`WebSocket` stay patch-enforced and
22
+ bare `node:net` imports remain a documented residual), and the browser opaque
23
+ origin sandbox (`sandbox="allow-scripts"` iframe hosting a classic Worker
24
+ `type:"module"` workers cannot load in opaque origins, verified). Capability
25
+ tier (NOT a security boundary): `SandboxedScriptExecutor` /
26
+ `BrowserWorkerScriptExecutor` in blob-Worker mode, with deny-by-default
27
+ network policy, builtin-module allowlist, forced approval, timeouts.
24
28
  - **`navigator.webskill`** — a browser facade (`discover` / `read` / `validate` /
25
29
  `run` / `install` / `uninstall`) assembled explicitly in one call.
26
30
  - **MCP** — call page-provided tools and consume page-declared dynamic skills
@@ -102,7 +106,8 @@ const run = await api.run('Summarize the references of skill greeter.');
102
106
 
103
107
  ### `@webskill/sdk/node`
104
108
 
105
- Node host: `NodeFS`, script executors (in-process + `worker_threads` sandbox),
109
+ Node host: `NodeFS`, script executors (in-process, `worker_threads` sandbox, and
110
+ `ProcessSandboxExecutor` for real process isolation),
106
111
  file-backed stores, `SkillManager` (install/export incl. multi-skill packs),
107
112
  CLI UI bridge, schema inference.
108
113
 
@@ -117,9 +122,10 @@ console.log((await manager.verifyIntegrity('greeter')).ok);
117
122
 
118
123
  ### `@webskill/sdk/browser`
119
124
 
120
- Browser host: OPFS provider, Web Worker script sandbox (deny-by-default network
121
- policy), worker runtime host/client, `BrowserSkillManager`, and the
122
- `navigator.webskill` assembler. See
125
+ Browser host: OPFS provider, opaque origin iframe script sandbox (default on
126
+ the main thread; engines running inside a Worker automatically fall back to the
127
+ blob Worker form — no iframe is available there, documented), worker runtime
128
+ host/client, `BrowserSkillManager`, and the `navigator.webskill` assembler. See
123
129
  [`examples/quickstart-browser`](../examples/quickstart-browser).
124
130
 
125
131
  ```js
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as FileStat, I as SkillInstallSource, K as VerifyResult, W as SkillsLockfile, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CgNC-oQu-DEcIBJKG.js";
2
- import { N as NetworkPolicy, St as parseBridgeRequest, X as ScriptExecutionContext, Z as ScriptExecutor, ct as WebSkillApi, d as BridgeCapabilities, dt as bridgeError, et as ToolDefinition, m as BridgeResponse, nt as ToolResult, p as BridgeRequest, u as ApprovalScope, v as ExternalSkillProvider, y as ExternalToolSource } from "./index-COuOu6gw.js";
1
+ import { C as FileStat, I as SkillInstallSource, K as VerifyResult, W as SkillsLockfile, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CKm5G_eQ-8W8FnP4u.js";
2
+ import { N as NetworkPolicy, Q as ScriptExecutor, Z as ScriptExecutionContext, d as BridgeCapabilities, it as ToolResult, m as BridgeResponse, nt as ToolDefinition, p as BridgeRequest, pt as bridgeError, u as ApprovalScope, ut as WebSkillApi, v as ExternalSkillProvider, wt as parseBridgeRequest, y as ExternalToolSource } from "./index-gjFuBevI.js";
3
3
  //#region ../browser/dist/index.d.ts
4
4
  //#region src/fs/featureDetection.d.ts
5
5
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -24,6 +24,7 @@ declare class OpfsProvider implements FileSystemProvider {
24
24
  remove(p: string, options?: {
25
25
  recursive?: boolean;
26
26
  }): Promise<void>;
27
+ rename(from: string, to: string): Promise<void>;
27
28
  }
28
29
  //#endregion
29
30
  //#region src/executor/workerBootstrap.d.ts
@@ -122,6 +123,8 @@ interface WorkerLike {
122
123
  terminate(): void;
123
124
  }
124
125
  type WorkerFactory = () => WorkerLike;
126
+ /** 沙箱形态:opaque iframe(默认,主线程可用 document 时)/ blob Worker(Worker 内引擎回退) */
127
+ type SandboxMode = 'iframe' | 'worker' | 'auto';
125
128
  /**
126
129
  * Web Worker 脚本沙箱执行器:loadDefinition 与 execute 都在 Worker 内完成,
127
130
  * 主线程从不 import 技能脚本;每次执行独立 Worker;超时 terminate 强杀。
@@ -132,6 +135,12 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
132
135
  constructor(deps: {
133
136
  fs: FileSystemProvider;
134
137
  workerFactory?: WorkerFactory;
138
+ /**
139
+ * 沙箱形态:'auto'(默认)——主线程用 opaque origin iframe(sandbox=allow-scripts srcdoc,
140
+ * 真实隔离);Worker 内引擎自动回退 blob Worker(无 iframe 可用,文档明示)。
141
+ * 显式 'worker' 强制 blob Worker(0.1.x 旧形态)。
142
+ */
143
+ sandbox?: SandboxMode;
135
144
  capabilities?: BridgeCapabilities;
136
145
  /** 网络策略:默认 'deny-all'(白名单见 runtime/sandbox/networkPolicy) */
137
146
  networkPolicy?: NetworkPolicy;
@@ -152,6 +161,31 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
152
161
  }): Promise<ToolResult>;
153
162
  }
154
163
  //#endregion
164
+ //#region src/executor/iframeSandbox.d.ts
165
+ /**
166
+ * opaque origin 沙箱 iframe(sandbox="allow-scripts" srcdoc):
167
+ * iframe 内建 module Worker(Blob URL),iframe window 作 postMessage 中继。
168
+ * opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS(独立 storage 分区)。
169
+ * 就绪握手:iframe 加载完成发 sandbox-ready 后才放行出站消息(防丢失)。
170
+ */
171
+ declare class IframeWorkerLike implements WorkerLike {
172
+ #private;
173
+ constructor(iframe: HTMLIFrameElement);
174
+ get ready(): boolean;
175
+ markReady(): void;
176
+ postMessage(data: unknown): void;
177
+ addEventListener(_type: 'message', listener: (event: {
178
+ data: unknown;
179
+ }) => void): void;
180
+ emit(data: unknown): void;
181
+ terminate(): void;
182
+ }
183
+ /**
184
+ * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 module Worker。
185
+ * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
186
+ */
187
+ declare function createIframeWorker(bootstrapSource: string, doc?: Document): IframeWorkerLike;
188
+ //#endregion
155
189
  //#region src/navigator.d.ts
156
190
  declare global {
157
191
  /** 0.0.6:installWebSkillNavigator 显式装配后可用(不自动挂全局) */
@@ -342,4 +376,4 @@ declare class WorkerUiBridge implements UiBridge {
342
376
  onTextDelta(runId: string, delta: string): Promise<void>;
343
377
  }
344
378
  //#endregion
345
- export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserSkillManager, BrowserWorkerScriptExecutor, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SerializedError, TsTranspiler, type TypeScriptSupportOptions, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
379
+ export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserSkillManager, BrowserWorkerScriptExecutor, IframeWorkerLike, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SandboxMode, type SerializedError, TsTranspiler, type TypeScriptSupportOptions, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, createIframeWorker, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
- import { A as validateSkills, C as parseSkillPackManifest, O as resolveInsideRoot, S as parseSkillMarkdown, c as SkillDiscovery, f as buildCatalog, i as SKILL_MANIFEST_FILE, j as verifyManifest, k as unzipWithLimits, p as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, v as exportSkills, w as readResponseWithLimit, y as isValidSkillName } from "./dist-fZFZaf43.js";
2
- import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DuGN5x0v.js";
3
- import { n as MockLlmClient } from "./testing-BmR48Pn1.js";
1
+ import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, T as readResponseWithLimit, b as isValidSkillName, c as SkillDiscovery, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, p as buildCatalog, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D0saNPi_.js";
2
+ import { A as isNetworkAllowed, C as bridgeError, E as createWebSkillApi, M as networkUrlHost, N as normalizeToolContent, P as parseBridgeRequest, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as mergeCatalogEntries, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DrySSQ5R.js";
3
+ import { n as MockLlmClient } from "./testing-B4pq6JYa.js";
4
4
 
5
5
  //#region ../browser/dist/index.js
6
6
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -132,6 +132,11 @@ var OpfsProvider = class {
132
132
  await (await this.#walkDir(`/${segments.join("/")}`, false)).removeEntry(name, { recursive: options?.recursive ?? false });
133
133
  });
134
134
  }
135
+ async rename(from, to) {
136
+ if ((await this.stat(from)).type === "directory") throw new WebSkillError("FS_PERMISSION_DENIED", `rename of directories is not supported on OPFS: ${from}`);
137
+ await this.writeBinary(to, await this.readBinary(from));
138
+ await this.remove(from);
139
+ }
135
140
  };
136
141
  /**
137
142
  * Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
@@ -700,21 +705,114 @@ var BrowserSkillManager = class {
700
705
  async #upsertLockEntry(name, entry) {
701
706
  const lockfile = await this.listInstalled();
702
707
  lockfile.skills[name] = entry;
703
- await this.#fs.writeText(lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
708
+ await atomicWriteText(this.#fs, lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
704
709
  }
705
710
  async #removeLockEntry(name) {
706
711
  const lockfile = await this.listInstalled();
707
712
  delete lockfile.skills[name];
708
- await this.#fs.writeText(lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
713
+ await atomicWriteText(this.#fs, lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
714
+ }
715
+ };
716
+ const ENVELOPE = "__webskill_sandbox__";
717
+ /**
718
+ * opaque origin 沙箱 iframe(sandbox="allow-scripts" srcdoc):
719
+ * iframe 内建 module Worker(Blob URL),iframe window 作 postMessage 中继。
720
+ * opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS(独立 storage 分区)。
721
+ * 就绪握手:iframe 加载完成发 sandbox-ready 后才放行出站消息(防丢失)。
722
+ */
723
+ var IframeWorkerLike = class {
724
+ #iframe;
725
+ #ready = false;
726
+ #queue = [];
727
+ constructor(iframe) {
728
+ this.#iframe = iframe;
729
+ }
730
+ get ready() {
731
+ return this.#ready;
732
+ }
733
+ markReady() {
734
+ this.#ready = true;
735
+ for (const payload of this.#queue) this.#post(payload);
736
+ this.#queue = [];
737
+ }
738
+ #post(payload) {
739
+ this.#iframe.contentWindow?.postMessage({
740
+ [ENVELOPE]: true,
741
+ payload
742
+ }, "*");
743
+ }
744
+ postMessage(data) {
745
+ if (this.#ready) this.#post(data);
746
+ else this.#queue.push(data);
747
+ }
748
+ addEventListener(_type, listener) {
749
+ this.#listeners.add(listener);
750
+ }
751
+ #listeners = /* @__PURE__ */ new Set();
752
+ emit(data) {
753
+ for (const listener of this.#listeners) listener({ data });
754
+ }
755
+ terminate() {
756
+ this.#iframe.remove();
709
757
  }
710
758
  };
711
- const defaultWorkerFactory = () => {
759
+ /** srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断) */
760
+ function sandboxDocument(bootstrapSource) {
761
+ const bootstrapJson = JSON.stringify(bootstrapSource).replace(/<\//g, "<\\/");
762
+ return `<!doctype html>
763
+ <html><head><meta charset="utf-8"></head><body><script>
764
+ const ENVELOPE = ${JSON.stringify(ENVELOPE)};
765
+ const blob = new Blob([${bootstrapJson}], { type: 'text/javascript' });
766
+ const url = URL.createObjectURL(blob);
767
+ // 注意:opaque origin 下 module Worker 无法加载(实证),classic Worker + data: URL 动态导入可用
768
+ const worker = new Worker(url);
769
+ URL.revokeObjectURL(url);
770
+ window.addEventListener('message', (event) => {
771
+ const data = event.data;
772
+ if (data && data[ENVELOPE]) worker.postMessage(data.payload);
773
+ });
774
+ worker.addEventListener('message', (event) => {
775
+ parent.postMessage({ [ENVELOPE]: true, payload: event.data }, '*');
776
+ });
777
+ worker.addEventListener('error', (event) => {
778
+ parent.postMessage({ [ENVELOPE]: true, payload: { type: 'sandbox-worker-error', message: event.message } }, '*');
779
+ });
780
+ parent.postMessage({ [ENVELOPE]: true, payload: { type: 'sandbox-ready' } }, '*');
781
+ <\/script></body></html>`;
782
+ }
783
+ /**
784
+ * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 module Worker。
785
+ * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
786
+ */
787
+ function createIframeWorker(bootstrapSource, doc = document) {
788
+ const iframe = doc.createElement("iframe");
789
+ iframe.setAttribute("sandbox", "allow-scripts");
790
+ iframe.style.display = "none";
791
+ iframe.srcdoc = sandboxDocument(bootstrapSource);
792
+ const worker = new IframeWorkerLike(iframe);
793
+ (doc.defaultView ?? window).addEventListener("message", (event) => {
794
+ const data = event.data;
795
+ if (!data || data[ENVELOPE] !== true) return;
796
+ if (data.payload && data.payload.type === "sandbox-ready") {
797
+ worker.markReady();
798
+ return;
799
+ }
800
+ worker.emit(data.payload);
801
+ });
802
+ doc.body.appendChild(iframe);
803
+ return worker;
804
+ }
805
+ const blobWorkerFactory = () => {
712
806
  const blob = new Blob([WORKER_BOOTSTRAP_SOURCE], { type: "text/javascript" });
713
807
  const url = URL.createObjectURL(blob);
714
808
  const worker = new Worker(url, { type: "module" });
715
809
  URL.revokeObjectURL(url);
716
810
  return worker;
717
811
  };
812
+ const resolveWorkerFactory = (mode) => {
813
+ if (mode === "iframe" || mode === "auto" && typeof document !== "undefined") return () => createIframeWorker(WORKER_BOOTSTRAP_SOURCE);
814
+ return blobWorkerFactory;
815
+ };
718
816
  const baseName = (p) => p.split("/").pop() ?? p;
719
817
  const messageOf = (e) => e instanceof Error ? e.message : String(e);
720
818
  /**
@@ -731,7 +829,7 @@ var BrowserWorkerScriptExecutor = class {
731
829
  #transpiler;
732
830
  constructor(deps) {
733
831
  this.#fs = deps.fs;
734
- this.#workerFactory = deps.workerFactory ?? defaultWorkerFactory;
832
+ this.#workerFactory = deps.workerFactory ?? resolveWorkerFactory(deps.sandbox ?? "auto");
735
833
  this.#transpiler = deps.typescript ? new TsTranspiler({
736
834
  fs: deps.fs,
737
835
  options: deps.typescript
@@ -1478,4 +1576,4 @@ var WorkerRuntimeClient = class {
1478
1576
  };
1479
1577
 
1480
1578
  //#endregion
1481
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
1579
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, createIframeWorker, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
@@ -1,4 +1,4 @@
1
- import { u as WebSkillError } from "./dist-fZFZaf43.js";
1
+ import { u as WebSkillError } from "./dist-D0saNPi_.js";
2
2
 
3
3
  //#region ../ui/dist/index.js
4
4
  /** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
@@ -524,6 +524,8 @@ var WebFormBridge = class {
524
524
  #doc;
525
525
  #styles;
526
526
  #progressEl;
527
+ /** 等待中的 request 取消句柄(id → 触发 cancelled 并卸载 DOM) */
528
+ #pending = /* @__PURE__ */ new Map();
527
529
  constructor(options) {
528
530
  this.#mount = options.mount;
529
531
  this.#doc = options.document ?? options.mount.ownerDocument;
@@ -535,13 +537,27 @@ var WebFormBridge = class {
535
537
  const model = interactionToFormModel(input);
536
538
  const { form, cleanup } = this.#renderForm(model);
537
539
  return new Promise((resolve) => {
540
+ const cancel = () => {
541
+ cleanup();
542
+ this.#pending.delete(input.id);
543
+ resolve({
544
+ id: input.id,
545
+ cancelled: true
546
+ });
547
+ };
548
+ this.#pending.set(input.id, cancel);
538
549
  form.addEventListener("submit", (event) => {
539
550
  event.preventDefault();
540
551
  const { values, missingRequired } = collectValues(model.controls, form);
541
552
  if (missingRequired.length > 0) {
542
- for (const name of missingRequired) form.querySelector(`[${CONTROL_TAG}="${name}"]`)?.closest(".webskill-form__control")?.classList.add("webskill-form__control--invalid");
553
+ for (const name of missingRequired) {
554
+ const wrapper = form.querySelector(`[${CONTROL_TAG}="${name}"]`)?.closest(".webskill-form__control");
555
+ wrapper?.classList.add("webskill-form__control--invalid");
556
+ wrapper?.querySelector(".webskill-form__input")?.setAttribute("aria-invalid", "true");
557
+ }
543
558
  return;
544
559
  }
560
+ this.#pending.delete(input.id);
545
561
  cleanup();
546
562
  resolve({
547
563
  id: input.id,
@@ -550,15 +566,15 @@ var WebFormBridge = class {
550
566
  });
551
567
  form.querySelector("[data-webskill-cancel]")?.addEventListener("click", (event) => {
552
568
  event.preventDefault();
553
- cleanup();
554
- resolve({
555
- id: input.id,
556
- cancelled: true
557
- });
569
+ cancel();
558
570
  });
559
571
  this.#mount.appendChild(form);
560
572
  });
561
573
  }
574
+ /** 尽力取消等待中的 request(超时后清理遗留表单) */
575
+ cancel(id) {
576
+ this.#pending.get(id)?.();
577
+ }
562
578
  async renderResult(input) {
563
579
  if (this.#styles) ensureStyles(this.#doc);
564
580
  this.#clearProgress();
@@ -622,9 +638,12 @@ var WebFormBridge = class {
622
638
  const doc = this.#doc;
623
639
  const wrapper = doc.createElement("div");
624
640
  wrapper.className = "webskill-form__control";
641
+ const controlId = `webskill-field-${control.name}`;
642
+ const errorId = `webskill-error-${control.name}`;
625
643
  const label = doc.createElement("label");
626
644
  label.className = control.required ? "webskill-form__label webskill-form__label-required" : "webskill-form__label";
627
645
  label.textContent = control.label;
646
+ label.htmlFor = controlId;
628
647
  wrapper.appendChild(label);
629
648
  if (control.description) {
630
649
  const desc = doc.createElement("div");
@@ -663,9 +682,13 @@ var WebFormBridge = class {
663
682
  input = textInput;
664
683
  }
665
684
  input.setAttribute(CONTROL_TAG, control.name);
685
+ input.id = controlId;
686
+ input.setAttribute("aria-describedby", errorId);
666
687
  wrapper.appendChild(input);
667
688
  const error = doc.createElement("div");
668
689
  error.className = "webskill-form__error";
690
+ error.id = errorId;
691
+ error.setAttribute("role", "alert");
669
692
  error.textContent = "This field is required";
670
693
  wrapper.appendChild(error);
671
694
  return wrapper;