@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.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 +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** #1629: parent-owned Canvas Worker transport; the Chat adapter owns product admission. */
|
|
2
|
+
import { maySendEffectEvent } from "./effect-policy.js";
|
|
3
|
+
const plain = v => !!v && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
4
|
+
const exact = (v, keys) => plain(v) && Object.keys(v).sort().join(",") === keys.slice().sort().join(",");
|
|
5
|
+
const finite = v => typeof v === "number" && Number.isFinite(v);
|
|
6
|
+
const bytes = v => new TextEncoder().encode(v).byteLength;
|
|
7
|
+
|
|
8
|
+
/** Snapshot only host-validated code, primitives and raster bytes; no HTML or DOM interface exists. */
|
|
9
|
+
function snapshotRender(raw) {
|
|
10
|
+
if (!exact(raw, ["code", "params", "instance", "width", "height", "dpr", "assets", "wants"]) || typeof raw.code !== "string" || !plain(raw.params) || !Array.isArray(raw.assets) || raw.assets.length > 16 || !Array.isArray(raw.wants) || Array.from(raw.wants).some(v => !["pointer", "state", "reply", "mention", "theme"].includes(v)) || new Set(raw.wants).size !== raw.wants.length) return null;
|
|
11
|
+
if (raw.instance !== null && (typeof raw.instance !== "string" || raw.instance.length < 1 || raw.instance.length > 128)) return null;
|
|
12
|
+
if (!Number.isInteger(raw.width) || raw.width < 1 || raw.width > 4096 || !Number.isInteger(raw.height) || raw.height < 1 || raw.height > 4096 || raw.width * raw.height > 8388608 || !finite(raw.dpr) || raw.dpr < 0.5 || raw.dpr > 3) return null;
|
|
13
|
+
const params = Object.create(null), keys = Object.keys(raw.params);
|
|
14
|
+
if (keys.length > 16) return null;
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
const value = raw.params[key];
|
|
17
|
+
if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || !(typeof value === "boolean" || finite(value) || typeof value === "string" && value.length <= 128)) return null;
|
|
18
|
+
params[key] = value;
|
|
19
|
+
}
|
|
20
|
+
let total = bytes(raw.code) + bytes(JSON.stringify(params));
|
|
21
|
+
if (total > 1048576) return null;
|
|
22
|
+
const assets = [], names = new Set();
|
|
23
|
+
for (const a of raw.assets) {
|
|
24
|
+
if (!exact(a, ["name", "mime", "bytes"]) || typeof a.name !== "string" || !/^assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif)$/.test(a.name) || names.has(a.name) || !["image/png", "image/jpeg", "image/webp", "image/gif"].includes(a.mime) || !(a.bytes instanceof Uint8Array)) return null;
|
|
25
|
+
total += a.bytes.byteLength;
|
|
26
|
+
if (total > 1048576) return null;
|
|
27
|
+
names.add(a.name);
|
|
28
|
+
assets.push({ name: a.name, mime: a.mime, bytes: new Uint8Array(a.bytes) });
|
|
29
|
+
}
|
|
30
|
+
return { code: raw.code, params: { ...params }, instance: raw.instance, width: raw.width, height: raw.height, dpr: raw.dpr, assets, wants: [...raw.wants] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* All options come from the host, never from an agent event or manifest origin. The registry supplies an admitted
|
|
35
|
+
* root and geometry in a later integration. This primitive alone does not implement caps, clipping or profiling.
|
|
36
|
+
*/
|
|
37
|
+
export function createEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}) {
|
|
38
|
+
let url;
|
|
39
|
+
try { url = new URL(origin); } catch { return { ok: false, reason: "preview_unknown" }; }
|
|
40
|
+
if (origin !== url.origin || !["http:", "https:"].includes(url.protocol) || typeof boot !== "string" || !/^[0-9a-f]{32}$/.test(boot)) return { ok: false, reason: "preview_unknown" };
|
|
41
|
+
let hostOrigin; try { hostOrigin = new URL(host.location.origin); } catch { return { ok: false, reason: "preview_unknown" }; }
|
|
42
|
+
if (!["http:", "https:"].includes(hostOrigin.protocol) || origin === hostOrigin.origin || hostOrigin.protocol === "https:" && url.protocol !== "https:") return { ok: false, reason: "preview_unknown" };
|
|
43
|
+
const payload = snapshotRender(render);
|
|
44
|
+
if (!payload || !host || !root || root.ownerDocument !== host.document || typeof root.append !== "function" || typeof host.MessageChannel !== "function" || typeof clock !== "function" || typeof onReady !== "function" || typeof onRemove !== "function") return { ok: false, reason: "unsupported" };
|
|
45
|
+
let mounted;
|
|
46
|
+
try { mounted = clock(); } catch { return { ok: false, reason: "clock_unavailable" }; }
|
|
47
|
+
if (!finite(mounted) || mounted < 0) return { ok: false, reason: "clock_unavailable" };
|
|
48
|
+
const frame = host.document.createElement("iframe");
|
|
49
|
+
frame.setAttribute("sandbox", "allow-scripts");
|
|
50
|
+
frame.setAttribute("tabindex", "-1");
|
|
51
|
+
frame.setAttribute("aria-hidden", "true");
|
|
52
|
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
53
|
+
frame.className = "agend-effect-frame";
|
|
54
|
+
let removed = false, ready = false, transferred = false, loads = 0, channel = null, timer = null, last = mounted;
|
|
55
|
+
const time = () => {
|
|
56
|
+
let now; try { now = clock(); } catch { return null; }
|
|
57
|
+
if (!finite(now) || now < last) return null;
|
|
58
|
+
last = now; return now;
|
|
59
|
+
};
|
|
60
|
+
const state = () => ({ ready, removed, loads });
|
|
61
|
+
const remove = reason => {
|
|
62
|
+
if (removed) return;
|
|
63
|
+
removed = true;
|
|
64
|
+
host.removeEventListener("message", hello);
|
|
65
|
+
frame.removeEventListener("load", load);
|
|
66
|
+
if (timer !== null) host.clearTimeout(timer);
|
|
67
|
+
if (channel) {
|
|
68
|
+
channel.port1.removeEventListener("message", portReady);
|
|
69
|
+
try { channel.port1.close(); } catch { /* already detached */ }
|
|
70
|
+
try { channel.port2.close(); } catch { /* already transferred */ }
|
|
71
|
+
}
|
|
72
|
+
frame.remove();
|
|
73
|
+
try { onRemove(reason); } catch { /* host callback cannot revive a removed frame */ }
|
|
74
|
+
};
|
|
75
|
+
const load = () => { loads++; if (loads > 1) remove("navigation"); };
|
|
76
|
+
const current = () => {
|
|
77
|
+
if (removed) return false;
|
|
78
|
+
if (frame.parentNode !== root || frame.isConnected !== true) { remove("detached"); return false; }
|
|
79
|
+
const now = time();
|
|
80
|
+
if (now === null) { remove("clock_unavailable"); return false; }
|
|
81
|
+
if (!ready && now - mounted >= 10000) { remove("startup_timeout"); return false; }
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const portReady = event => {
|
|
85
|
+
if (removed || ready) return;
|
|
86
|
+
let data; try { data = event.data; } catch { return; }
|
|
87
|
+
if (!exact(data, ["type"]) || data.type !== "ready") return;
|
|
88
|
+
if (!current()) return;
|
|
89
|
+
ready = true;
|
|
90
|
+
host.clearTimeout(timer); timer = null;
|
|
91
|
+
channel.port1.removeEventListener("message", portReady);
|
|
92
|
+
try { onReady(); } catch { remove("host_callback"); }
|
|
93
|
+
};
|
|
94
|
+
const hello = event => {
|
|
95
|
+
if (removed || transferred) return;
|
|
96
|
+
let data; try { data = event.data; } catch { return; }
|
|
97
|
+
// Opaque sandbox messages have origin null. Only this exact WindowProxy and boot may receive the private port.
|
|
98
|
+
if (event.source !== frame.contentWindow || event.origin !== "null" || event.ports?.length || !exact(data, ["v", "type", "ch", "boot"]) || data.v !== 1 || data.type !== "ready" || data.ch !== null || data.boot !== boot) return;
|
|
99
|
+
if (!current()) return;
|
|
100
|
+
transferred = true;
|
|
101
|
+
host.removeEventListener("message", hello);
|
|
102
|
+
try {
|
|
103
|
+
channel = new host.MessageChannel();
|
|
104
|
+
channel.port1.addEventListener("message", portReady);
|
|
105
|
+
channel.port1.start();
|
|
106
|
+
const nonce = new Uint8Array(16); host.crypto.getRandomValues(nonce);
|
|
107
|
+
const ch = Array.from(nonce, b => b.toString(16).padStart(2, "0")).join("");
|
|
108
|
+
const { wants, ...message } = payload;
|
|
109
|
+
// '*' is required for the opaque destination; the source WindowProxy was checked immediately above.
|
|
110
|
+
frame.contentWindow.postMessage({ v: 1, type: "render", ch, ...message }, "*", [channel.port2]);
|
|
111
|
+
} catch { remove("transport"); }
|
|
112
|
+
};
|
|
113
|
+
const deadline = () => {
|
|
114
|
+
if (!current() || ready) return;
|
|
115
|
+
timer = host.setTimeout(deadline, Math.max(1, 10000 - (last - mounted)));
|
|
116
|
+
};
|
|
117
|
+
host.addEventListener("message", hello);
|
|
118
|
+
frame.addEventListener("load", load);
|
|
119
|
+
timer = host.setTimeout(deadline, 10000);
|
|
120
|
+
try { frame.src = `${origin}/effect`; root.append(frame); } catch { remove("mount_failed"); }
|
|
121
|
+
return { ok: true, element: frame, state, remove,
|
|
122
|
+
send(event) {
|
|
123
|
+
if (!ready || !current() || !maySendEffectEvent(event, { instance: payload.instance, wants: payload.wants })) return false;
|
|
124
|
+
try { channel.port1.postMessage(event); return true; } catch { remove("transport"); return false; }
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** #1629: host-only fixed viewport geometry and trust holes through the already-loaded, fixed same-origin app stylesheet. No inline attributes. */
|
|
2
|
+
import { effectHostStyle } from "./effect-host-style.js";
|
|
3
|
+
import { effectTrustClip } from "./effect-policy.js";
|
|
4
|
+
import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
|
|
5
|
+
let serial = 0;
|
|
6
|
+
// Native CSSOM canonicalizes SVG command whitespace. Compare bounded tokens, never whitespace-stripped digits.
|
|
7
|
+
function sameClip(expected, written) {
|
|
8
|
+
if (expected === "none") return written === "none";
|
|
9
|
+
const tokens = value => {
|
|
10
|
+
if (typeof value !== "string" || value.length > 100000) return null;
|
|
11
|
+
const m = /^path\(\s*evenodd\s*,\s*["']([^"']*)["']\s*\)$/.exec(value);
|
|
12
|
+
if (!m) return null;
|
|
13
|
+
const out = [], rest = m[1].replace(/[MHVZ]|-?\d+(?:\.\d+)?/g, token => { out.push(/[MHVZ]/.test(token) ? token : Number(token)); return ""; });
|
|
14
|
+
return /^[\s,]*$/.test(rest) ? JSON.stringify(out) : null;
|
|
15
|
+
};
|
|
16
|
+
const a = tokens(expected), b = tokens(written);
|
|
17
|
+
return a !== null && a === b;
|
|
18
|
+
}
|
|
19
|
+
const number = n => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 1e7;
|
|
20
|
+
/** A fixed frame must share viewport axes. Unknown transformed/filter/contained ancestors fail closed. */
|
|
21
|
+
export function hostEffectSpace(host, element) {
|
|
22
|
+
try {
|
|
23
|
+
if (element?.ownerDocument !== host.document || element.isConnected !== true || typeof host.getComputedStyle !== "function") return false;
|
|
24
|
+
let el = element, seen = 0;
|
|
25
|
+
while (el) {
|
|
26
|
+
if (++seen > 128 || el.ownerDocument !== host.document) return false;
|
|
27
|
+
const s = host.getComputedStyle(el);
|
|
28
|
+
for (const key of ["transform", "perspective", "filter", "backdropFilter", "rotate", "scale", "translate", "offsetPath"]) if (s[key] !== undefined && s[key] !== "" && s[key] !== "none") return false;
|
|
29
|
+
if (s.contain !== "none" || s.willChange !== "auto" || s.contentVisibility !== undefined && s.contentVisibility !== "" && s.contentVisibility !== "visible" || s.zoom !== undefined && !["", "normal", "1"].includes(s.zoom)) return false;
|
|
30
|
+
if (el === host.document.documentElement) return true;
|
|
31
|
+
el = el.parentElement;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
} catch { return false; }
|
|
35
|
+
}
|
|
36
|
+
export function hostEffectRect(element) {
|
|
37
|
+
try {
|
|
38
|
+
if (element?.isConnected !== true) return null;
|
|
39
|
+
const r = element.getBoundingClientRect();
|
|
40
|
+
if (![r.x, r.y, r.width, r.height].every(number) || r.width < 1 || r.height < 1) return null;
|
|
41
|
+
const x = Math.floor(r.x), y = Math.floor(r.y), width = Math.ceil(r.x + r.width) - x, height = Math.ceil(r.y + r.height) - y;
|
|
42
|
+
return width <= 4096 && height <= 4096 && width * height <= 8388608 ? { x, y, width, height } : null;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
export function createEffectGeometry({ window: host } = {}) {
|
|
46
|
+
let sheet;
|
|
47
|
+
try {
|
|
48
|
+
if (!host?.document || host.CSS?.supports("clip-path", 'path(evenodd, "M0 0H1V1H0Z")') !== true) return { ok: false, reason: "geometry_unsupported" };
|
|
49
|
+
sheet = effectHostStyle(host); if (!sheet) throw Error("app stylesheet unavailable");
|
|
50
|
+
} catch { return { ok: false, reason: "geometry_unsupported" }; }
|
|
51
|
+
const bound = new Set(); let disposed = false;
|
|
52
|
+
function bind(frame, { layer, by } = {}) {
|
|
53
|
+
if (disposed || !hostEffectSpace(host, frame) || !["front", "behind"].includes(layer) || !frame.classList) {
|
|
54
|
+
// The caller must also close its factory handle. Never leave its own rejected frame painting.
|
|
55
|
+
if (frame?.ownerDocument === host.document && typeof frame.remove === "function") frame.remove();
|
|
56
|
+
return { ok: false, reason: "geometry_unknown" };
|
|
57
|
+
}
|
|
58
|
+
const token = `agend-effect-geometry-${++serial}`;
|
|
59
|
+
let rule;
|
|
60
|
+
try {
|
|
61
|
+
const at = sheet.insertRule(`.${token}{}`, sheet.cssRules.length); rule = sheet.cssRules[at];
|
|
62
|
+
if (!rule?.style) throw Error("rule unavailable");
|
|
63
|
+
for (const [key, value] of Object.entries({ position: "fixed", border: "0px", margin: "0px", padding: "0px", "pointer-events": "none", "z-index": layer === "front" ? "20" : "0", opacity: layer === "front" && by !== "human" ? "0.85" : "1", visibility: "hidden", "background-color": "transparent", "box-sizing": "border-box", "box-shadow": "none", outline: "none", transition: "none", animation: "none", "min-width": "0px", "min-height": "0px", "max-width": "none", "max-height": "none" })) rule.style.setProperty(key, value, "important");
|
|
64
|
+
frame.classList.add(token);
|
|
65
|
+
// Binding is accepted BEFORE the first sync/ready. Prove its initial hidden state before the caller may yield.
|
|
66
|
+
if (host.getComputedStyle(frame).visibility !== "hidden") throw Error("initial hiding cascade lost");
|
|
67
|
+
} catch {
|
|
68
|
+
try { const at = [...sheet.cssRules].indexOf(rule); if (at >= 0) sheet.deleteRule(at); } catch { /* cleanup cannot precede physical refusal */ }
|
|
69
|
+
try { frame.classList.remove(token); } catch { /* the refused frame will not remain in the document */ }
|
|
70
|
+
frame.remove();
|
|
71
|
+
return { ok: false, reason: "geometry_unknown" };
|
|
72
|
+
}
|
|
73
|
+
let dead = false;
|
|
74
|
+
const hide = () => {
|
|
75
|
+
try {
|
|
76
|
+
rule.style.setProperty("visibility", "hidden", "important");
|
|
77
|
+
if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") frame.remove();
|
|
78
|
+
} catch { frame.remove(); }
|
|
79
|
+
};
|
|
80
|
+
const handle = { ok: true, hide,
|
|
81
|
+
/** Same-turn host paint snapshot is mandatory, including behind/human placements. No DOM fallback. */
|
|
82
|
+
sync(rect, stock) {
|
|
83
|
+
hide();
|
|
84
|
+
const proof = effectStockSnapshot(stock);
|
|
85
|
+
if (!effectStockAllows(proof, { layer, by })) return false;
|
|
86
|
+
if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) { hide(); return false; }
|
|
87
|
+
let path = "none";
|
|
88
|
+
if (layer === "front" && by !== "human") {
|
|
89
|
+
const clipped = effectTrustClip(rect, proof.rects);
|
|
90
|
+
if (!clipped.ok) return false;
|
|
91
|
+
path = clipped.path;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
for (const key of ["x", "y", "width", "height"]) rule.style.setProperty(key === "x" ? "left" : key === "y" ? "top" : key, `${rect[key]}px`, "important");
|
|
96
|
+
rule.style.setProperty("clip-path", path, "important");
|
|
97
|
+
if (!sameClip(path, rule.style.getPropertyValue("clip-path"))) return false;
|
|
98
|
+
// The stylesheet can lose the cascade to another host rule. Verify the actual viewport and clip
|
|
99
|
+
// before revealing: holes expressed in planned coordinates cannot protect a transformed frame.
|
|
100
|
+
const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
|
|
101
|
+
if (style.position !== "fixed" || style.pointerEvents !== "none" || style.visibility !== "hidden" || style.opacity !== (layer === "front" && by !== "human" ? "0.85" : "1") || style.zIndex !== (layer === "front" ? "20" : "0") || !sameClip(path, style.clipPath) || !["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)) return false;
|
|
102
|
+
rule.style.setProperty("visibility", "visible", "important");
|
|
103
|
+
return true;
|
|
104
|
+
} catch { hide(); return false; }
|
|
105
|
+
},
|
|
106
|
+
remove() {
|
|
107
|
+
if (dead) return; dead = true; hide(); bound.delete(handle);
|
|
108
|
+
if (frame.isConnected === true) frame.remove();
|
|
109
|
+
try { frame.classList.remove(token); const at = [...sheet.cssRules].indexOf(rule); if (at >= 0) sheet.deleteRule(at); } catch { frame.remove(); }
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
bound.add(handle); return handle;
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, bind,
|
|
115
|
+
hide() { for (const h of bound) h.hide(); },
|
|
116
|
+
dispose() {
|
|
117
|
+
if (disposed) return; disposed = true;
|
|
118
|
+
for (const h of [...bound]) h.remove();
|
|
119
|
+
// The host link and its other rules remain intact. Each binding removed only its own rule.
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Fixed same-origin host stylesheet only. No new sheet, inline style, owner CSS or supplied URL. */
|
|
2
|
+
export function effectHostStyle(host) {
|
|
3
|
+
try {
|
|
4
|
+
const link = host?.document?.querySelector('link[rel="stylesheet"][href="/assets/app.css"]');
|
|
5
|
+
const href = new URL("/assets/app.css", host.location.origin).href;
|
|
6
|
+
if (!link || link.tagName !== "LINK" || link.ownerDocument !== host.document || link.isConnected !== true || link.disabled !== false || !link.sheet || link.sheet.ownerNode !== link || link.sheet.href !== href || link.sheet.disabled !== false) return null;
|
|
7
|
+
const sheet = link.sheet;
|
|
8
|
+
return typeof sheet.insertRule === "function" && typeof sheet.deleteRule === "function" && sheet.cssRules ? sheet : null;
|
|
9
|
+
} catch { return null; }
|
|
10
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** #1629: host-only personal region targets on the currently proven stock shell. No selection, code or frame authority. */
|
|
2
|
+
import { createEffectGeometry, hostEffectRect, hostEffectSpace } from "./effect-geometry.js";
|
|
3
|
+
import { effectStockSnapshot } from "./effect-stock-snapshot.js";
|
|
4
|
+
const finite = n => typeof n === "number" && Number.isFinite(n);
|
|
5
|
+
function viewport(host) {
|
|
6
|
+
const width = host.innerWidth, height = host.innerHeight;
|
|
7
|
+
return [width, height].every(n => Number.isInteger(n) && n >= 1 && n <= 4096) && width * height <= 8388608 ? { x: 0, y: 0, width, height } : null;
|
|
8
|
+
}
|
|
9
|
+
/** An anchored box uses edge padding; center ignores the offset. Oversized boxes shrink within that padding. */
|
|
10
|
+
export function ownerEffectBoxRect(box, size) {
|
|
11
|
+
try {
|
|
12
|
+
if (!box || ![Object.prototype, null].includes(Object.getPrototypeOf(box))) return null;
|
|
13
|
+
const fields = Object.getOwnPropertyDescriptors(box), keys = Reflect.ownKeys(fields);
|
|
14
|
+
if (keys.length !== 4 || !["anchor", "width", "height", "offset"].every(k => fields[k] && Object.hasOwn(fields[k], "value"))) return null;
|
|
15
|
+
const b = Object.fromEntries(Object.entries(fields).map(([k, d]) => [k, d.value]));
|
|
16
|
+
if (!["top-left", "top-right", "bottom-left", "bottom-right", "center"].includes(b.anchor) || ![b.width, b.height].every(n => Number.isInteger(n) && n >= 32 && n <= 480) || !Number.isInteger(b.offset) || b.offset < 0 || b.offset > 64 || !size || ![size.width, size.height].every(n => Number.isInteger(n) && n >= 1 && n <= 4096) || size.width * size.height > 8388608) return null;
|
|
17
|
+
const pad = b.anchor === "center" ? 0 : b.offset, width = Math.min(b.width, size.width - pad * 2), height = Math.min(b.height, size.height - pad * 2);
|
|
18
|
+
if (width < 1 || height < 1) return null;
|
|
19
|
+
const x = b.anchor === "center" ? Math.floor((size.width - width) / 2) : b.anchor.endsWith("right") ? size.width - pad - width : pad;
|
|
20
|
+
const y = b.anchor === "center" ? Math.floor((size.height - height) / 2) : b.anchor.startsWith("bottom") ? size.height - pad - height : pad;
|
|
21
|
+
return { x, y, width, height };
|
|
22
|
+
} catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
/** Caller supplies the authenticated lease and fresh stock proof. Header/sidebar are real stock elements;
|
|
25
|
+
* page/box are empty hidden host DOM references, never author DOM. They consume no frame/cap slot.
|
|
26
|
+
* A null result requires removing that descriptor; this primitive cannot retire a caller-owned effect frame. */
|
|
27
|
+
export function createOwnerEffectRegions({ window: host, current, factories = {} } = {}) {
|
|
28
|
+
let dead = false, root = null, geometry = null;
|
|
29
|
+
const targets = new Map();
|
|
30
|
+
const dispose = () => { if (dead) return; dead = true; geometry?.dispose?.(); root?.remove(); targets.clear(); };
|
|
31
|
+
const refuse = () => { for (const row of targets.values()) row.binding.hide(); return null; };
|
|
32
|
+
const unavailable = () => ({ ok: false, dispose });
|
|
33
|
+
const alive = () => { if (dead) return false; const valid = current() === true; return !dead && valid && host.document.body.dataset.mode === "full"; };
|
|
34
|
+
try {
|
|
35
|
+
if (!host?.document || typeof current !== "function" || typeof host.getComputedStyle !== "function" || !alive()) return unavailable();
|
|
36
|
+
root = host.document.createElement("div"); root.className = "agend-effect-layer"; host.document.body.append(root);
|
|
37
|
+
geometry = (factories.geometry ?? createEffectGeometry)({ window: host }); if (!geometry.ok) { dispose(); return unavailable(); }
|
|
38
|
+
} catch { dispose(); return unavailable(); }
|
|
39
|
+
function panel() {
|
|
40
|
+
const nodes = host.document.querySelectorAll(".main > .panel"); if (nodes.length !== 1) return null;
|
|
41
|
+
const el = nodes[0], kind = el.getAttribute("data-effect-panel");
|
|
42
|
+
return kind === null ? el.classList.contains("p-chat") ? el : null
|
|
43
|
+
: ["fleet", "needs", "settings", "view", "log", "details"].includes(kind) && el.classList.contains(`p-${kind}`) ? el : null;
|
|
44
|
+
}
|
|
45
|
+
function resolve(slot, box, stock) {
|
|
46
|
+
try {
|
|
47
|
+
if (!alive() || !effectStockSnapshot(stock)) return refuse();
|
|
48
|
+
const surface = panel(), size = viewport(host); if (!surface || !size) return refuse();
|
|
49
|
+
if (slot === "fx.header" || slot === "fx.sidebar") {
|
|
50
|
+
const selector = slot === "fx.header" ? ".main > .panel > .panel-head[data-trust=header]" : ".shell > .sidebar";
|
|
51
|
+
const nodes = host.document.querySelectorAll(selector); if (nodes.length !== 1) return null;
|
|
52
|
+
const el = nodes[0], s = host.getComputedStyle(el), rect = hostEffectRect(el);
|
|
53
|
+
return alive() && panel() === surface && s.display !== "none" && s.visibility === "visible" && hostEffectSpace(host, el) && rect && rect.x + rect.width > 0 && rect.y + rect.height > 0 && rect.x < size.width && rect.y < size.height ? el : null;
|
|
54
|
+
}
|
|
55
|
+
if (!["fx.page", "fx.box"].includes(slot)) return null;
|
|
56
|
+
const rect = slot === "fx.page" ? size : ownerEffectBoxRect(box, size); if (!rect) return refuse();
|
|
57
|
+
let row = targets.get(slot);
|
|
58
|
+
if (!row) {
|
|
59
|
+
const element = host.document.createElement("div"); element.className = "agend-owner-effect-target"; element.setAttribute("aria-hidden", "true"); root.append(element);
|
|
60
|
+
const binding = geometry.bind(element, { layer: "behind", by: "human" });
|
|
61
|
+
if (!binding.ok) { element.remove(); return null; }
|
|
62
|
+
row = { element, binding }; targets.set(slot, row);
|
|
63
|
+
}
|
|
64
|
+
// Empty reference only: the synchronous existing hidden-first verification is followed by hide before yielding.
|
|
65
|
+
// The future registry owns the actual effect's layer/provenance/caps and independent fresh paint check.
|
|
66
|
+
if (!row.binding.sync(rect, stock)) { row.binding.remove(); targets.delete(slot); return null; }
|
|
67
|
+
row.binding.hide();
|
|
68
|
+
const actual = hostEffectRect(row.element);
|
|
69
|
+
if (!alive() || panel() !== surface || row.element.isConnected !== true || host.getComputedStyle(row.element).visibility !== "hidden" || !actual || !["x", "y", "width", "height"].every(k => finite(actual[k]) && actual[k] === rect[k])) { row.binding.remove(); targets.delete(slot); return null; }
|
|
70
|
+
return row.element;
|
|
71
|
+
} catch { return refuse(); }
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, root, resolve, dispose };
|
|
74
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** #1629: host-only personal contribution model. No fetch, timer, DOM creation, registry or playback. */
|
|
2
|
+
import { checkEffectOwnerSlots } from "./effect-runtime-loader.js";
|
|
3
|
+
import { effectStockSnapshot } from "./effect-stock-snapshot.js";
|
|
4
|
+
const SLOTS = ["fx.page", "fx.header", "fx.sidebar", "fx.box"], ALL_SLOTS = [...SLOTS, "fx.cursor"];
|
|
5
|
+
|
|
6
|
+
/** The app supplies its one loader/metadata scheduler, lifetime and fixed stock target resolver. */
|
|
7
|
+
export function createOwnerEffectSource({ current, readMetadata, readResource, resolveTarget, stock, onChange = () => {}, onUnavailable = () => {} } = {}) {
|
|
8
|
+
let dead = false, generation = 0, serial = 0, metadata = null, error = null, active = null, rev = -1, checking = false, cursor = null;
|
|
9
|
+
const bySlot = new Map(), byKey = new Map(), paused = new Map(), failed = new Map();
|
|
10
|
+
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
11
|
+
function alive() {
|
|
12
|
+
if (dead || checking) return false;
|
|
13
|
+
const before = generation; checking = true;
|
|
14
|
+
let value; try { value = current() === true; } catch { value = false; } finally { checking = false; }
|
|
15
|
+
return !dead && before === generation && value;
|
|
16
|
+
}
|
|
17
|
+
const fresh = (body, gen) => alive() && generation === gen && metadata === body;
|
|
18
|
+
function drop(slot) { const b = bySlot.get(slot); if (b) byKey.delete(b.key); bySlot.delete(slot); }
|
|
19
|
+
function clear() { bySlot.clear(); byKey.clear(); }
|
|
20
|
+
const notify = () => { if (alive()) safe(onChange); };
|
|
21
|
+
function dispose() { if (dead) return; dead = true; generation++; metadata = null; cursor = null; clear(); paused.clear(); failed.clear(); }
|
|
22
|
+
const unavailable = { ok: false, dispose };
|
|
23
|
+
if (![current, readMetadata, readResource, resolveTarget, stock, onChange, onUnavailable].every(x => typeof x === "function")) return unavailable;
|
|
24
|
+
function row(slot) { return alive() && SLOTS.includes(slot) ? metadata?.slots[slot] ?? null : null; }
|
|
25
|
+
function bindingCurrent(b) {
|
|
26
|
+
if (!b || !alive() || generation !== b.generation || bySlot.get(b.slot) !== b || byKey.get(b.key) !== b || failed.get(b.slot) === b.id) return false;
|
|
27
|
+
const selected = metadata?.slots[b.slot];
|
|
28
|
+
return selected?.status === "available" && selected.id === b.id && b.element.isConnected === true && !dead && generation === b.generation;
|
|
29
|
+
}
|
|
30
|
+
function cursorSelection() {
|
|
31
|
+
const body = metadata, gen = generation;
|
|
32
|
+
if (!alive() || !body || error) return null;
|
|
33
|
+
const selected = body.slots["fx.cursor"];
|
|
34
|
+
if (selected.status !== "available" || failed.get("fx.cursor") === selected.id || !selected.manifest.cursor) return null;
|
|
35
|
+
if (!cursor || cursor.id !== selected.id || cursor.generation !== gen) cursor = Object.freeze({ id: selected.id, generation: gen, hotspot: Object.freeze([...selected.manifest.cursor.hotspot]) });
|
|
36
|
+
return fresh(body, gen) && failed.get("fx.cursor") !== selected.id ? cursor : null;
|
|
37
|
+
}
|
|
38
|
+
function cursorCurrent(value) {
|
|
39
|
+
try {
|
|
40
|
+
const expected = cursorSelection(); if (!expected || !value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return false;
|
|
41
|
+
const d = Object.getOwnPropertyDescriptors(value);
|
|
42
|
+
if (Reflect.ownKeys(d).length !== 3 || !["id", "generation", "hotspot"].every(k => d[k] && Object.hasOwn(d[k], "value")) || d.id.value !== expected.id || d.generation.value !== expected.generation) return false;
|
|
43
|
+
const a = d.hotspot.value; if (!Array.isArray(a) || Object.getPrototypeOf(a) !== Array.prototype) return false;
|
|
44
|
+
const h = Object.getOwnPropertyDescriptors(a);
|
|
45
|
+
return Reflect.ownKeys(h).length === 3 && h.length.value === 2 && [0, 1].every(k => h[k] && Object.hasOwn(h[k], "value") && h[k].value === expected.hotspot[k]) && cursorSelection() === expected;
|
|
46
|
+
} catch { return false; }
|
|
47
|
+
}
|
|
48
|
+
function refuse(b) {
|
|
49
|
+
if (!bindingCurrent(b)) return;
|
|
50
|
+
failed.set(b.slot, b.id); drop(b.slot); notify();
|
|
51
|
+
if (!dead && generation === b.generation && failed.get(b.slot) === b.id && metadata?.slots[b.slot]?.id === b.id) safe(() => onUnavailable(b.slot, b.id));
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, cursorSelection, cursorCurrent,
|
|
54
|
+
refuseCursor(value) {
|
|
55
|
+
const own = cursorSelection(); if (!own || !cursorCurrent(value)) return;
|
|
56
|
+
failed.set("fx.cursor", own.id); notify();
|
|
57
|
+
if (!dead && generation === own.generation && failed.get("fx.cursor") === own.id && metadata?.slots["fx.cursor"]?.id === own.id) safe(() => onUnavailable("fx.cursor", own.id));
|
|
58
|
+
},
|
|
59
|
+
needsMetadata() { return alive() && metadata === null && error === null; },
|
|
60
|
+
invalidate() { if (dead) return; generation++; metadata = null; cursor = null; error = null; clear(); failed.clear(); notify(); },
|
|
61
|
+
async read(signal) {
|
|
62
|
+
if (!alive() || metadata !== null || error !== null || active || signal?.aborted) return false;
|
|
63
|
+
const op = { generation }; active = op;
|
|
64
|
+
const valid = () => alive() && generation === op.generation && active === op && !signal?.aborted;
|
|
65
|
+
try {
|
|
66
|
+
const answer = await readMetadata(signal); if (!valid()) return false;
|
|
67
|
+
const next = checkEffectOwnerSlots(answer);
|
|
68
|
+
if (!next || next.rev < rev) { error = next ? "stale" : "unreadable"; clear(); notify(); return false; }
|
|
69
|
+
rev = next.rev; metadata = next;
|
|
70
|
+
for (const [slot, id] of paused) if (next.slots[slot]?.id !== id) paused.delete(slot);
|
|
71
|
+
notify(); return valid() && metadata === next;
|
|
72
|
+
} catch { if (valid()) { error = "unreadable"; clear(); notify(); } return false; }
|
|
73
|
+
finally { if (active === op) active = null; }
|
|
74
|
+
},
|
|
75
|
+
descriptors() {
|
|
76
|
+
if (!alive() || !metadata || error) return [];
|
|
77
|
+
const body = metadata, gen = generation, proof = safe(() => effectStockSnapshot(stock()));
|
|
78
|
+
if (!fresh(body, gen)) return [];
|
|
79
|
+
if (!proof) { clear(); return []; }
|
|
80
|
+
const result = [];
|
|
81
|
+
for (const slot of SLOTS) {
|
|
82
|
+
const selected = body.slots[slot];
|
|
83
|
+
if (selected.status !== "available" || failed.get(slot) === selected.id) { drop(slot); continue; }
|
|
84
|
+
const element = safe(() => resolveTarget(slot, selected.manifest, proof));
|
|
85
|
+
if (!fresh(body, gen)) return [];
|
|
86
|
+
if (!element || element.isConnected !== true) { drop(slot); continue; }
|
|
87
|
+
let b = bySlot.get(slot);
|
|
88
|
+
if (!b || b.id !== selected.id || b.element !== element || b.generation !== gen) {
|
|
89
|
+
drop(slot); b = Object.freeze({ kind: "owner", slot, id: selected.id, key: `owner:${++serial}:${slot}:${selected.id}`, generation: gen, element });
|
|
90
|
+
bySlot.set(slot, b); byKey.set(b.key, b);
|
|
91
|
+
}
|
|
92
|
+
result.push({ key: b.key, id: b.id, by: "human", region: slot.slice(3), layer: selected.manifest.layer, order: 0, paused: paused.get(slot) === b.id, visible: true, instance: null, element });
|
|
93
|
+
}
|
|
94
|
+
return fresh(body, gen) ? result : [];
|
|
95
|
+
},
|
|
96
|
+
binding(key, id) { const b = byKey.get(key); return b?.id === id && bindingCurrent(b) ? b : null; },
|
|
97
|
+
slotBinding(slot) { const b = bySlot.get(slot); return bindingCurrent(b) ? b : null; },
|
|
98
|
+
current: bindingCurrent,
|
|
99
|
+
async load(b, signal) {
|
|
100
|
+
if (!bindingCurrent(b) || signal?.aborted) return null;
|
|
101
|
+
let value; try { value = await readResource({ id: b.id, slot: b.slot }, signal); } catch { value = null; }
|
|
102
|
+
if (!bindingCurrent(b) || signal?.aborted) return null;
|
|
103
|
+
if (!value) { refuse(b); return null; }
|
|
104
|
+
return value;
|
|
105
|
+
},
|
|
106
|
+
pause(slot) { const selected = row(slot); if (selected?.status !== "available") return false; paused.set(slot, selected.id); notify(); return true; },
|
|
107
|
+
unpause(slot) { const selected = row(slot); if (selected?.status !== "available") return false; paused.delete(slot); notify(); return true; },
|
|
108
|
+
snapshot() {
|
|
109
|
+
const live = alive();
|
|
110
|
+
return { error: live ? error : null, rev, loading: live && metadata === null && error === null,
|
|
111
|
+
slots: Object.fromEntries(ALL_SLOTS.map(slot => { const selected = live ? metadata?.slots[slot] : null; return [slot, selected ? failed.get(slot) === selected.id ? { status: "unavailable", id: selected.id } : selected : { status: "unknown", id: null }]; })) };
|
|
112
|
+
},
|
|
113
|
+
dispose,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** #1629 r3: pure host policy. No frame creation, DOM reads, content reads or network. */
|
|
2
|
+
import { effectTrialStageIdentity } from "./effect-trial-stage.js";
|
|
3
|
+
const OWN = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
4
|
+
const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
|
|
5
|
+
const finite = v => typeof v === "number" && Number.isFinite(v);
|
|
6
|
+
const instanceName = v => typeof v === "string" && v.length >= 1 && v.length <= 128;
|
|
7
|
+
const TYPES = Object.freeze({ state: ["state"], reply: [], mention: [], theme: ["mode"], env: ["width", "height", "dpr", "quality"], pointer: ["kind", "x", "y", "buttons"] });
|
|
8
|
+
/** Reserved for host-drawn widgets, never exposed to a Canvas Worker. */
|
|
9
|
+
export const WIDGET_SIGNAL_KINDS = Object.freeze(["status", "usage", "fleet"]);
|
|
10
|
+
|
|
11
|
+
/** Build a fresh content-free signal. Unknown fields are never copied or read. */
|
|
12
|
+
export function effectEvent(type, fields, instance, ts) {
|
|
13
|
+
if (!OWN(TYPES, type) || !plain(fields) || !finite(ts) || ts < 0 || instance !== null && !instanceName(instance)) return null;
|
|
14
|
+
const out = { v: 1, type, ts };
|
|
15
|
+
if (["state", "reply", "mention"].includes(type)) {
|
|
16
|
+
if (OWN(fields, "instance") && fields.instance !== instance) return null;
|
|
17
|
+
if (instance !== null) out.instance = instance;
|
|
18
|
+
}
|
|
19
|
+
for (const key of TYPES[type]) out[key] = fields[key];
|
|
20
|
+
if (type === "pointer") for (const key of ["dx", "dy"]) if (OWN(fields, key)) out[key] = fields[key];
|
|
21
|
+
return validEffectEvent(out, instance) ? out : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Independent send fence: exact keys, enums, numeric bounds and the current region's own instance. */
|
|
25
|
+
export function validEffectEvent(d, instance) {
|
|
26
|
+
if (instance !== null && !instanceName(instance)) return false;
|
|
27
|
+
if (!plain(d) || d.v !== 1 || !OWN(TYPES, d.type) || !finite(d.ts) || d.ts < 0) return false;
|
|
28
|
+
const keys = ["v", "type", "ts", ...TYPES[d.type]];
|
|
29
|
+
if (["state", "reply", "mention"].includes(d.type) && OWN(d, "instance")) {
|
|
30
|
+
if (!instanceName(instance) || d.instance !== instance) return false;
|
|
31
|
+
keys.push("instance");
|
|
32
|
+
}
|
|
33
|
+
if (d.type === "pointer") for (const key of ["dx", "dy"]) if (OWN(d, key)) {
|
|
34
|
+
if (!finite(d[key]) || Math.abs(d[key]) > 10000) return false;
|
|
35
|
+
keys.push(key);
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(d).sort().join(",") !== keys.sort().join(",")) return false;
|
|
38
|
+
switch (d.type) {
|
|
39
|
+
case "state": return ["idle", "working", "needs_you", "stopped"].includes(d.state);
|
|
40
|
+
case "reply": case "mention": return true;
|
|
41
|
+
case "theme": return ["light", "dark"].includes(d.mode);
|
|
42
|
+
case "env": return Number.isInteger(d.width) && d.width >= 1 && d.width <= 4096 && Number.isInteger(d.height) && d.height >= 1 && d.height <= 4096 && d.width * d.height <= 8388608 && finite(d.dpr) && d.dpr >= 0.5 && d.dpr <= 3 && ["normal", "low"].includes(d.quality);
|
|
43
|
+
case "pointer": return ["move", "down", "up", "click", "wheel"].includes(d.kind) && finite(d.x) && d.x >= 0 && d.x <= 4096 && finite(d.y) && d.y >= 0 && d.y <= 4096 && Number.isInteger(d.buttons) && d.buttons >= 0 && d.buttons <= 31;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Least privilege at the final send; the instance is the host's binding, never taken from event data. */
|
|
49
|
+
export function maySendEffectEvent(event, binding) {
|
|
50
|
+
if (!plain(binding) || !validEffectEvent(event, binding.instance) || !Array.isArray(binding.wants) || event.type === "mention") return false;
|
|
51
|
+
if (["state", "reply", "mention"].includes(event.type)) {
|
|
52
|
+
if (binding.instance === null ? OWN(event, "instance") : !OWN(event, "instance") || event.instance !== binding.instance) return false;
|
|
53
|
+
}
|
|
54
|
+
return event.type === "env" || binding.wants.includes(event.type);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function constrainedDevice(signals) {
|
|
58
|
+
return !!signals && (finite(signals.memory) && signals.memory > 0 && signals.memory <= 4 || finite(signals.cores) && signals.cores > 0 && signals.cores <= 4 || signals.coarse === true && finite(signals.width) && signals.width > 0 && signals.width <= 900);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const OWNER_ORDER = Object.freeze({ page: 0, header: 1, sidebar: 2, box: 3 });
|
|
62
|
+
/** Recomputed from host descriptors; queued/off/unknown descriptors never get a frame reservation. */
|
|
63
|
+
export function allocateEffectFrames(descriptors, context) {
|
|
64
|
+
if (!Array.isArray(descriptors) || descriptors.length > 128 || !plain(context)) return [];
|
|
65
|
+
const keys = new Set(), duplicate = new Set();
|
|
66
|
+
for (const d of descriptors) if (d && typeof d.key === "string") { if (keys.has(d.key)) duplicate.add(d.key); keys.add(d.key); }
|
|
67
|
+
const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "unknown", queuePos: null }));
|
|
68
|
+
const gates = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "previewReady", "optIn", "constrained", "modal"];
|
|
69
|
+
if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result;
|
|
70
|
+
let global = null;
|
|
71
|
+
if (context.safe === true) global = "safe_mode";
|
|
72
|
+
else if (context.publicLink === true) global = "public_link";
|
|
73
|
+
else if (context.reducedMotion === true) global = "reduced_motion";
|
|
74
|
+
else if (context.saveData === true) global = "save_data";
|
|
75
|
+
else if (context.hidden === true) global = "hidden";
|
|
76
|
+
else if (context.enabled !== true || context.show !== true) global = "disabled";
|
|
77
|
+
else if (context.previewReady !== true || context.optIn !== true) global = "preview_unavailable";
|
|
78
|
+
const candidates = [];
|
|
79
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
80
|
+
const d = descriptors[i], r = result[i];
|
|
81
|
+
if (!plain(d) || typeof d.key !== "string" || d.key.length < 1 || d.key.length > 256 || duplicate.has(d.key) || !/^[0-9a-f]{32}$/.test(d.id) || !["chat", "page", "header", "sidebar", "box"].includes(d.region) || !["front", "behind"].includes(d.layer) || !Number.isSafeInteger(d.order) || d.order < 0 || typeof d.paused !== "boolean") continue;
|
|
82
|
+
// Missing/unknown provenance always receives the agent restrictions.
|
|
83
|
+
const trialIdentity = d.trialStage === undefined || d.trialStage === null ? null : effectTrialStageIdentity(d.trialStage);
|
|
84
|
+
const trial = !!trialIdentity && d.region === "chat" && trialIdentity.key === d.key && trialIdentity.id === d.id && trialIdentity.instance === d.instance && trialIdentity.generation === d.generation;
|
|
85
|
+
if (d.trialStage != null && !trial) { r.reason = "trial_unknown"; continue; }
|
|
86
|
+
const owner = !trial && d.by === "human";
|
|
87
|
+
if (!owner && d.region !== "chat") { r.reason = "scope"; continue; }
|
|
88
|
+
if (d.region === "chat" && (!instanceName(d.instance) || d.instance !== context.openInstance)) { r.reason = "closed_chat"; continue; }
|
|
89
|
+
if (global) { r.reason = global; continue; }
|
|
90
|
+
if (d.paused === true) { r.reason = "paused"; continue; }
|
|
91
|
+
if (d.visible !== true) { r.reason = "offscreen"; continue; }
|
|
92
|
+
if (d.layer === "front" && context.modal === true && !trial) { r.reason = "modal"; continue; }
|
|
93
|
+
candidates.push({ i, d, priority: owner && OWN(OWNER_ORDER, d.region) ? OWNER_ORDER[d.region] : 4 });
|
|
94
|
+
}
|
|
95
|
+
candidates.sort((a, b) => a.priority - b.priority || a.d.order - b.d.order || a.i - b.i);
|
|
96
|
+
const max = context.constrained === true ? 1 : 3, frontMax = context.constrained === true ? 1 : 2;
|
|
97
|
+
let active = 0, front = 0, queued = 0;
|
|
98
|
+
for (const { i, d } of candidates) {
|
|
99
|
+
const r = result[i];
|
|
100
|
+
if (active >= max || d.layer === "front" && front >= frontMax) {
|
|
101
|
+
r.verdict = "queued"; r.reason = active >= max ? "frame_limit" : "front_limit"; r.queuePos = ++queued;
|
|
102
|
+
} else {
|
|
103
|
+
r.verdict = "active"; r.reason = null; active++; if (d.layer === "front") front++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const finiteRect = r => plain(r) && ["x", "y", "width", "height"].every(k => finite(r[k]) && Math.abs(r[k]) <= 1e7) && r.width >= 0 && r.height >= 0;
|
|
110
|
+
/**
|
|
111
|
+
* Integer padded holes are unioned before evenodd: overlapping holes must never repaint trusted pixels.
|
|
112
|
+
* Input is the host's viewport rects, not mod geometry. Unknown/overflow means hide, never an unclipped fallback.
|
|
113
|
+
*/
|
|
114
|
+
export function effectTrustClip(frame, trusted) {
|
|
115
|
+
const unknown = { ok: false, reason: "trust_geometry_unknown" };
|
|
116
|
+
if (!finiteRect(frame) || frame.width < 1 || frame.height < 1 || frame.width > 4096 || frame.height > 4096 || !Number.isInteger(frame.width) || !Number.isInteger(frame.height) || !Array.isArray(trusted) || trusted.length > 128) return unknown;
|
|
117
|
+
const rects = [];
|
|
118
|
+
for (const r of trusted) {
|
|
119
|
+
if (!finiteRect(r)) return unknown;
|
|
120
|
+
const x1 = Math.max(0, Math.floor(r.x - frame.x - 8)), y1 = Math.max(0, Math.floor(r.y - frame.y - 8));
|
|
121
|
+
const x2 = Math.min(frame.width, Math.ceil(r.x + r.width - frame.x + 8)), y2 = Math.min(frame.height, Math.ceil(r.y + r.height - frame.y + 8));
|
|
122
|
+
if (x2 > x1 && y2 > y1) rects.push({ x1, y1, x2, y2 });
|
|
123
|
+
}
|
|
124
|
+
const ys = [...new Set(rects.flatMap(r => [r.y1, r.y2]))].sort((a, b) => a - b), holes = [];
|
|
125
|
+
for (let i = 0; i + 1 < ys.length; i++) {
|
|
126
|
+
const y1 = ys[i], y2 = ys[i + 1], spans = rects.filter(r => r.y1 <= y1 && r.y2 >= y2).map(r => [r.x1, r.x2]).sort((a, b) => a[0] - b[0]);
|
|
127
|
+
let span = null;
|
|
128
|
+
for (const next of spans) {
|
|
129
|
+
if (span && next[0] <= span[1]) span[1] = Math.max(span[1], next[1]);
|
|
130
|
+
else { if (span) holes.push({ x: span[0], y: y1, width: span[1] - span[0], height: y2 - y1 }); span = [...next]; }
|
|
131
|
+
}
|
|
132
|
+
if (span) holes.push({ x: span[0], y: y1, width: span[1] - span[0], height: y2 - y1 });
|
|
133
|
+
if (holes.length > 512) return { ok: false, reason: "trust_geometry_limit" };
|
|
134
|
+
}
|
|
135
|
+
const rectPath = r => `M${r.x} ${r.y}H${r.x + r.width}V${r.y + r.height}H${r.x}Z`;
|
|
136
|
+
return { ok: true, holes, path: `path(evenodd, "${rectPath({ x: 0, y: 0, width: frame.width, height: frame.height })}${holes.map(rectPath).join("")}")` };
|
|
137
|
+
}
|