create-safest-tools 0.2.0
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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/create-safest-tools.mjs +12 -0
- package/package.json +34 -0
- package/src/cli.mjs +128 -0
- package/src/config.mjs +151 -0
- package/src/scaffold.mjs +30 -0
- package/template/LICENSE +201 -0
- package/template/README.md +35 -0
- package/template/console/README.md +28 -0
- package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
- package/template/console/analytics/controller.ts +133 -0
- package/template/console/analytics/events.ts +33 -0
- package/template/console/analytics/store.ts +33 -0
- package/template/console/analytics/types.ts +38 -0
- package/template/console/appeals/AppealWorkspace.tsx +73 -0
- package/template/console/appeals/controller.ts +125 -0
- package/template/console/appeals/events.ts +33 -0
- package/template/console/appeals/store.ts +35 -0
- package/template/console/appeals/types.ts +37 -0
- package/template/console/assistant/AssistantWorkspace.tsx +133 -0
- package/template/console/assistant/types.ts +34 -0
- package/template/console/auth/AccountJourney.tsx +69 -0
- package/template/console/auth/InvitationAcceptance.tsx +145 -0
- package/template/console/auth/OAuthButtons.tsx +78 -0
- package/template/console/auth/OperatorLogin.tsx +110 -0
- package/template/console/auth/PasswordRecovery.tsx +132 -0
- package/template/console/auth/account-route.ts +15 -0
- package/template/console/auth/browser-navigation.ts +7 -0
- package/template/console/auth/events.ts +15 -0
- package/template/console/command/CommandCentre.tsx +119 -0
- package/template/console/command/controller.ts +267 -0
- package/template/console/command/events.ts +19 -0
- package/template/console/command/store.ts +36 -0
- package/template/console/command/types.ts +61 -0
- package/template/console/components/AnalystIdentity.tsx +25 -0
- package/template/console/components/PageHeader.tsx +27 -0
- package/template/console/configuration/ConfigurationDialog.tsx +200 -0
- package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
- package/template/console/configuration/FormBuilder.tsx +143 -0
- package/template/console/configuration/api.ts +40 -0
- package/template/console/configuration/events.ts +14 -0
- package/template/console/configuration/types.ts +79 -0
- package/template/console/lib/format.ts +49 -0
- package/template/console/lib/http.ts +96 -0
- package/template/console/main.tsx +225 -0
- package/template/console/operations/OperationsWorkspace.tsx +126 -0
- package/template/console/operations/controller.ts +237 -0
- package/template/console/operations/events.ts +37 -0
- package/template/console/operations/store.ts +32 -0
- package/template/console/operations/types.ts +107 -0
- package/template/console/people/PeopleWorkspace.tsx +81 -0
- package/template/console/people/types.ts +25 -0
- package/template/console/profile/ProfileWorkspace.tsx +136 -0
- package/template/console/profile/events.ts +9 -0
- package/template/console/profile/types.ts +6 -0
- package/template/console/quality/QualityWorkspace.tsx +87 -0
- package/template/console/quality/controller.ts +154 -0
- package/template/console/quality/events.ts +34 -0
- package/template/console/quality/store.ts +36 -0
- package/template/console/quality/types.ts +70 -0
- package/template/console/queues/QueueEditor.tsx +173 -0
- package/template/console/queues/QueueWorkspace.tsx +109 -0
- package/template/console/queues/controller.ts +194 -0
- package/template/console/queues/events.ts +34 -0
- package/template/console/queues/store.ts +37 -0
- package/template/console/queues/types.ts +102 -0
- package/template/console/registry/RegistryDialog.tsx +198 -0
- package/template/console/registry/RegistryWorkspace.tsx +108 -0
- package/template/console/registry/events.ts +14 -0
- package/template/console/registry/types.ts +52 -0
- package/template/console/reports/ReportDrawer.tsx +159 -0
- package/template/console/reports/ReportWorkspace.tsx +90 -0
- package/template/console/reports/controller.ts +517 -0
- package/template/console/reports/events.ts +42 -0
- package/template/console/reports/store.ts +39 -0
- package/template/console/reports/types.ts +230 -0
- package/template/console/settings/BrandEditor.tsx +126 -0
- package/template/console/settings/ChannelDialog.tsx +241 -0
- package/template/console/settings/SettingsWorkspace.tsx +155 -0
- package/template/console/settings/events.ts +19 -0
- package/template/console/settings/handoff.ts +22 -0
- package/template/console/settings/types.ts +93 -0
- package/template/console/shell/WorkspaceShell.tsx +160 -0
- package/template/console/shell/controller.ts +182 -0
- package/template/console/shell/events.ts +25 -0
- package/template/console/shell/navigation.ts +61 -0
- package/template/console/shell/store.ts +56 -0
- package/template/console/shell/types.ts +86 -0
- package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
- package/template/console/workflows/WorkflowCanvas.tsx +42 -0
- package/template/console/workflows/WorkflowDialogs.tsx +6 -0
- package/template/console/workflows/WorkflowStudio.tsx +331 -0
- package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
- package/template/console/workflows/events.ts +17 -0
- package/template/console/workflows/graph.ts +156 -0
- package/template/console/workflows/templates.ts +70 -0
- package/template/console/workflows/types.ts +201 -0
- package/template/gitignore.template +18 -0
- package/template/migrations/0001_reports_foundation.sql +444 -0
- package/template/migrations/0002_human_report_loop.sql +65 -0
- package/template/migrations/0003_delivery_reliability.sql +18 -0
- package/template/migrations/0004_public_intake.sql +14 -0
- package/template/migrations/0005_operations_visibility.sql +24 -0
- package/template/migrations/0006_ai_governance.sql +198 -0
- package/template/migrations/0007_ai_release_gates.sql +6 -0
- package/template/migrations/0008_retention_analytics_exports.sql +52 -0
- package/template/migrations/0009_retention_derived_copies.sql +14 -0
- package/template/migrations/0010_ai_quality_controls.sql +26 -0
- package/template/migrations/0011_analyst_presence.sql +28 -0
- package/template/migrations/0012_queue_policies.sql +87 -0
- package/template/migrations/0013_routing_agents.sql +63 -0
- package/template/migrations/0014_webhook_enrichments.sql +122 -0
- package/template/migrations/0015_queue_owned_ai.sql +55 -0
- package/template/migrations/0016_operator_accounts.sql +67 -0
- package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
- package/template/migrations/0018_platform_configuration.sql +382 -0
- package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
- package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
- package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
- package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
- package/template/migrations/0023_component_connection_execution.sql +75 -0
- package/template/migrations/0024_access_runtime_integrity.sql +72 -0
- package/template/migrations/0025_installation_timezone.sql +11 -0
- package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
- package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
- package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
- package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
- package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
- package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
- package/template/migrations/0032_action_delivery_outbox.sql +55 -0
- package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
- package/template/migrations/0034_installation_integrations.sql +31 -0
- package/template/migrations/0035_workspace_governance.sql +21 -0
- package/template/migrations/0036_published_routing_baseline.sql +15 -0
- package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
- package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
- package/template/migrations/0039_secure_reporting_channels.sql +45 -0
- package/template/migrations/0040_notification_only_reporting.sql +24 -0
- package/template/migrations/0041_better_auth_credentials.sql +17 -0
- package/template/package.json +47 -0
- package/template/public/_headers +27 -0
- package/template/public/app-icon-192.png +0 -0
- package/template/public/app-icon-512.png +0 -0
- package/template/public/brand-icon.svg +7 -0
- package/template/public/brand-tokens.css +80 -0
- package/template/public/console/auth-shell.js +19340 -0
- package/template/public/customer-brand.js +58 -0
- package/template/public/embed/embed.css +139 -0
- package/template/public/embed/embed.js +408 -0
- package/template/public/embed/index.html +91 -0
- package/template/public/favicon.svg +7 -0
- package/template/public/fonts/Manrope-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Variable.ttf +0 -0
- package/template/public/index.html +123 -0
- package/template/public/logo-primary.svg +7 -0
- package/template/public/logo-reversed.svg +7 -0
- package/template/public/manifest.webmanifest +21 -0
- package/template/public/public-report.js +184 -0
- package/template/public/report/index.html +39 -0
- package/template/public/report/public-report.css +20 -0
- package/template/public/social-card.png +0 -0
- package/template/public/styles.css +1521 -0
- package/template/public/widget.css +80 -0
- package/template/public/widget.js +220 -0
- package/template/reports.config.example.json +38 -0
- package/template/reports.schema.json +89 -0
- package/template/scripts/reports-auth-onboarding.mjs +180 -0
- package/template/scripts/reports-backup.mjs +276 -0
- package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
- package/template/scripts/reports-deploy.mjs +173 -0
- package/template/scripts/reports-plan.mjs +226 -0
- package/template/scripts/reports-restore.mjs +180 -0
- package/template/scripts/reports-secrets.mjs +73 -0
- package/template/scripts/reports-uninstall-plan.mjs +32 -0
- package/template/src/agent-executor.ts +330 -0
- package/template/src/ai-observability.ts +163 -0
- package/template/src/ai-registry-validation.ts +244 -0
- package/template/src/ai-registry.ts +292 -0
- package/template/src/api-cursor.ts +84 -0
- package/template/src/assistant.ts +554 -0
- package/template/src/audit.ts +39 -0
- package/template/src/backup-service.ts +269 -0
- package/template/src/budget-control.ts +127 -0
- package/template/src/component-executor.ts +845 -0
- package/template/src/configuration-registry.ts +550 -0
- package/template/src/connection-egress.ts +430 -0
- package/template/src/connection-oauth.ts +368 -0
- package/template/src/evidence-service.ts +335 -0
- package/template/src/human-tasks.ts +316 -0
- package/template/src/index.ts +4114 -0
- package/template/src/installation-admin.ts +301 -0
- package/template/src/intake-abuse.ts +156 -0
- package/template/src/platform-registry-validation.ts +367 -0
- package/template/src/platform-registry.ts +753 -0
- package/template/src/report-admin.ts +342 -0
- package/template/src/report-ai-quality.ts +390 -0
- package/template/src/report-ai-validation.ts +80 -0
- package/template/src/report-ai.ts +857 -0
- package/template/src/report-auth.ts +292 -0
- package/template/src/report-better-auth.ts +583 -0
- package/template/src/report-context-schema.ts +132 -0
- package/template/src/report-context.ts +126 -0
- package/template/src/report-crypto.ts +142 -0
- package/template/src/report-delivery.ts +579 -0
- package/template/src/report-form-validation.ts +51 -0
- package/template/src/report-governance.ts +510 -0
- package/template/src/report-http.ts +90 -0
- package/template/src/report-operations.ts +685 -0
- package/template/src/report-operator-accounts.ts +810 -0
- package/template/src/report-presence.ts +398 -0
- package/template/src/report-queue-validation.ts +309 -0
- package/template/src/report-queues.ts +478 -0
- package/template/src/report-repository.ts +972 -0
- package/template/src/report-router-agent.ts +328 -0
- package/template/src/report-router-validation.ts +81 -0
- package/template/src/report-routing.ts +178 -0
- package/template/src/report-turnstile.ts +142 -0
- package/template/src/report-types.ts +224 -0
- package/template/src/report-validation.ts +279 -0
- package/template/src/report-workflow-validation.ts +119 -0
- package/template/src/report-workflow.ts +886 -0
- package/template/src/shadow-quality.ts +278 -0
- package/template/src/workflow-actions.ts +782 -0
- package/template/src/workflow-compiler.ts +230 -0
- package/template/src/workflow-dynamic-runtime.ts +613 -0
- package/template/src/workflow-effects.ts +316 -0
- package/template/src/workflow-expressions.ts +191 -0
- package/template/src/workflow-platform-types.ts +121 -0
- package/template/src/workflow-platform-validation.ts +540 -0
- package/template/src/workflow-repository.ts +916 -0
- package/template/src/workflow-runs.ts +686 -0
- package/template/src/workflow-simulator.ts +211 -0
- package/template/src/workspace-branding.ts +128 -0
- package/template/src/workspace-governance.ts +279 -0
- package/template/tsconfig.console.json +23 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.console.config.ts +20 -0
- package/template/worker-configuration.d.ts +65 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
(function customerBrandRuntime(global) {
|
|
2
|
+
const fallback = {
|
|
3
|
+
organization_name: "Safest Resolve", logo_url: "", logo_alt: "",
|
|
4
|
+
primary_color: "#154e3a", primary_contrast_color: "#ffffff", accent_color: "#cbef68",
|
|
5
|
+
background_color: "#f5f6f1", surface_color: "#ffffff", text_color: "#14231c",
|
|
6
|
+
muted_color: "#637168", border_color: "#dfe6e0",
|
|
7
|
+
body_font_family: '"Safest Sans", Manrope, ui-sans-serif, system-ui, sans-serif',
|
|
8
|
+
heading_font_family: '"Safest Serif", Newsreader, Georgia, serif', font_css_url: "", border_radius: 12,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function initials(value) {
|
|
12
|
+
return String(value || "SR").split(/\s+/u).slice(0, 2).map((part) => part[0] || "").join("").toUpperCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applyBrand(value, options = {}) {
|
|
16
|
+
const brand = { ...fallback, ...(value || {}) };
|
|
17
|
+
const defaultLogo = !brand.logo_url && brand.organization_name.trim().toLowerCase() === "safest resolve";
|
|
18
|
+
const logoUrl = brand.logo_url || (defaultLogo ? "/brand-icon.svg" : "");
|
|
19
|
+
const logoAlt = brand.logo_alt || (defaultLogo ? "Safest Resolve logo" : `${brand.organization_name} logo`);
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
const variables = {
|
|
22
|
+
"--forest": brand.primary_color, "--forest-dark": brand.primary_color,
|
|
23
|
+
"--brand-primary-contrast": brand.primary_contrast_color, "--brand-accent": brand.accent_color,
|
|
24
|
+
"--canvas": brand.background_color, "--paper": brand.surface_color, "--ink": brand.text_color,
|
|
25
|
+
"--muted": brand.muted_color, "--line": brand.border_color,
|
|
26
|
+
"--sans": brand.body_font_family, "--serif": brand.heading_font_family,
|
|
27
|
+
"--customer-radius": `${brand.border_radius}px`,
|
|
28
|
+
};
|
|
29
|
+
for (const [name, child] of Object.entries(variables)) root.style.setProperty(name, child);
|
|
30
|
+
for (const node of document.querySelectorAll("[data-customer-brand-name]")) node.textContent = brand.organization_name;
|
|
31
|
+
for (const node of document.querySelectorAll("[data-customer-brand-fallback]")) node.textContent = initials(brand.organization_name);
|
|
32
|
+
for (const image of document.querySelectorAll("[data-customer-logo]")) {
|
|
33
|
+
image.hidden = !logoUrl;
|
|
34
|
+
if (logoUrl) { image.src = logoUrl; image.alt = logoAlt; }
|
|
35
|
+
}
|
|
36
|
+
for (const node of document.querySelectorAll("[data-customer-logo-fallback]")) node.hidden = Boolean(logoUrl);
|
|
37
|
+
document.querySelector("link[data-customer-font]")?.remove();
|
|
38
|
+
if (brand.font_css_url) {
|
|
39
|
+
const link = document.createElement("link");
|
|
40
|
+
link.rel = "stylesheet";
|
|
41
|
+
link.href = brand.font_css_url;
|
|
42
|
+
link.dataset.customerFont = "true";
|
|
43
|
+
document.head.append(link);
|
|
44
|
+
}
|
|
45
|
+
if (options.title) document.title = `${options.title} — ${brand.organization_name}`;
|
|
46
|
+
global.dispatchEvent(new CustomEvent("safest-brand-applied", { detail: { brand } }));
|
|
47
|
+
return brand;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadBrand(path = "/v1/brand", options = {}) {
|
|
51
|
+
const response = await fetch(path, { headers: { accept: "application/json" }, credentials: "omit", referrerPolicy: "no-referrer" });
|
|
52
|
+
const result = await response.json().catch(() => ({}));
|
|
53
|
+
if (!response.ok) throw new Error(result?.error?.message || "Brand settings are unavailable.");
|
|
54
|
+
return applyBrand(result.brand, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
global.SafestBrand = { applyBrand, loadBrand, defaults: fallback };
|
|
58
|
+
})(globalThis);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
@font-face { font-family: Manrope; src: url("/fonts/Manrope-Variable.ttf") format("truetype"); font-weight: 200 800; font-display: swap; }
|
|
2
|
+
@font-face { font-family: Newsreader; src: url("/fonts/Newsreader-Variable.ttf") format("truetype"); font-weight: 300 700; font-display: swap; }
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: light;
|
|
6
|
+
--primary: #184c3d;
|
|
7
|
+
--on-primary: #fffdf7;
|
|
8
|
+
--accent: #d47a3b;
|
|
9
|
+
--page: #f6f8f5;
|
|
10
|
+
--surface: #fffdf9;
|
|
11
|
+
--text: #18221d;
|
|
12
|
+
--muted: #65736c;
|
|
13
|
+
--line: #dbe4de;
|
|
14
|
+
--radius: 20px;
|
|
15
|
+
--body: Manrope, ui-sans-serif, system-ui, sans-serif;
|
|
16
|
+
--heading: Newsreader, Georgia, serif;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
20
|
+
html { background: transparent; scrollbar-gutter: stable; }
|
|
21
|
+
body { margin: 0; min-width: 280px; background: transparent; color: var(--text); font-family: var(--body); -webkit-font-smoothing: antialiased; }
|
|
22
|
+
button, input, select, textarea { font: inherit; }
|
|
23
|
+
button, a { -webkit-tap-highlight-color: transparent; }
|
|
24
|
+
[hidden] { display: none !important; }
|
|
25
|
+
|
|
26
|
+
.report-card { min-height: 100vh; padding: 0 0 4px; background: var(--surface); }
|
|
27
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 24px 16px; border-bottom: 1px solid color-mix(in srgb, var(--line) 80%, transparent); }
|
|
28
|
+
.brand { display: flex; align-items: center; min-width: 0; gap: 10px; }
|
|
29
|
+
.brand img, .brand-fallback { width: 34px; height: 34px; flex: 0 0 auto; border-radius: 10px; object-fit: contain; }
|
|
30
|
+
.brand-fallback { display: grid; place-items: center; background: var(--primary); color: var(--on-primary); font-size: 11px; font-weight: 800; letter-spacing: .06em; }
|
|
31
|
+
.brand-copy { display: grid; min-width: 0; }
|
|
32
|
+
.brand-copy strong { overflow: hidden; color: var(--text); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
|
33
|
+
.brand-copy small { color: var(--muted); font-size: 10px; }
|
|
34
|
+
.privacy-mark { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; padding: 7px 10px; border: 1px solid color-mix(in srgb, var(--primary) 14%, var(--line)); border-radius: 999px; background: color-mix(in srgb, var(--primary) 5%, var(--surface)); color: var(--primary); font-size: 10px; font-weight: 750; }
|
|
35
|
+
.privacy-mark span { font-size: 7px; }
|
|
36
|
+
|
|
37
|
+
form { padding: 26px 28px 24px; }
|
|
38
|
+
.intro { max-width: 560px; }
|
|
39
|
+
.eyebrow { margin: 0 0 7px; color: var(--accent); font-size: 10px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
|
|
40
|
+
h1 { margin: 0; color: var(--text); font-family: var(--heading); font-size: clamp(28px, 5vw, 39px); font-weight: 560; letter-spacing: -.025em; line-height: 1.02; }
|
|
41
|
+
.intro > p:last-child { margin: 11px 0 0; max-width: 54ch; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
42
|
+
|
|
43
|
+
.trust-note { display: flex; gap: 11px; margin: 22px 0 8px; padding: 13px 14px; border: 1px solid color-mix(in srgb, var(--primary) 15%, var(--line)); border-radius: calc(var(--radius) * .72); background: color-mix(in srgb, var(--primary) 5%, var(--surface)); }
|
|
44
|
+
.trust-icon { display: grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto; border-radius: 50%; background: var(--primary); color: var(--on-primary); font-size: 12px; font-weight: 900; }
|
|
45
|
+
.trust-note div { display: grid; gap: 2px; }
|
|
46
|
+
.trust-note strong { color: var(--text); font-size: 12px; }
|
|
47
|
+
.trust-note p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
|
|
48
|
+
|
|
49
|
+
.form-section { margin-top: 27px; padding-top: 25px; border-top: 1px solid var(--line); }
|
|
50
|
+
.section-heading { display: flex; align-items: flex-start; gap: 11px; margin-bottom: 14px; }
|
|
51
|
+
.section-heading > span { display: grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto; border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--line)); border-radius: 8px; color: var(--primary); font-size: 10px; font-weight: 850; }
|
|
52
|
+
.section-heading h2 { margin: 1px 0 2px; color: var(--text); font-size: 14px; font-weight: 760; letter-spacing: -.01em; }
|
|
53
|
+
.section-heading p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
|
|
54
|
+
|
|
55
|
+
fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
56
|
+
.reason-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
|
|
57
|
+
.reason-option { position: relative; display: flex; min-height: 48px; cursor: pointer; }
|
|
58
|
+
.reason-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
59
|
+
.reason-option span { display: flex; align-items: center; width: 100%; padding: 11px 13px; border: 1px solid var(--line); border-radius: calc(var(--radius) * .65); background: var(--surface); color: var(--text); font-size: 12px; font-weight: 650; line-height: 1.3; transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease, transform 150ms ease; }
|
|
60
|
+
.reason-option span::before { content: ""; width: 12px; height: 12px; margin-right: 9px; flex: 0 0 auto; border: 1.5px solid color-mix(in srgb, var(--muted) 55%, var(--line)); border-radius: 50%; box-shadow: inset 0 0 0 3px var(--surface); }
|
|
61
|
+
.reason-option:hover span { border-color: color-mix(in srgb, var(--primary) 32%, var(--line)); transform: translateY(-1px); }
|
|
62
|
+
.reason-option input:checked + span { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 6%, var(--surface)); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 9%, transparent); }
|
|
63
|
+
.reason-option input:checked + span::before { border-color: var(--primary); background: var(--primary); }
|
|
64
|
+
.reason-option input:focus-visible + span { outline: 3px solid color-mix(in srgb, var(--primary) 24%, transparent); outline-offset: 2px; }
|
|
65
|
+
|
|
66
|
+
.answer-fields { display: grid; gap: 13px; }
|
|
67
|
+
.answer-field { display: grid; gap: 6px; color: var(--text); font-size: 11px; font-weight: 700; }
|
|
68
|
+
.answer-field .field-label { display: flex; justify-content: space-between; gap: 12px; }
|
|
69
|
+
.answer-field .optional { color: var(--muted); font-size: 9px; font-weight: 600; }
|
|
70
|
+
.answer-field small { color: var(--muted); font-size: 10px; font-weight: 500; line-height: 1.45; }
|
|
71
|
+
input[type="text"], input[type="email"], input[type="url"], input[type="number"], select, textarea { width: 100%; border: 1px solid var(--line); border-radius: calc(var(--radius) * .65); background: color-mix(in srgb, var(--page) 45%, var(--surface)); color: var(--text); outline: none; transition: border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; }
|
|
72
|
+
input[type="text"], input[type="email"], input[type="url"], input[type="number"], select { min-height: 44px; padding: 10px 12px; }
|
|
73
|
+
textarea { min-height: 118px; resize: vertical; padding: 12px; line-height: 1.55; }
|
|
74
|
+
input:focus, select:focus, textarea:focus { border-color: var(--primary); background: var(--surface); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent); }
|
|
75
|
+
|
|
76
|
+
.contact-section { margin: 20px 0 2px; padding: 15px; border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--line)); border-radius: calc(var(--radius) * .72); background: color-mix(in srgb, var(--primary) 4%, var(--surface)); }
|
|
77
|
+
.contact-section label { display: grid; gap: 7px; }
|
|
78
|
+
.contact-section .field-label { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--text); font-size: 12px; }
|
|
79
|
+
.contact-section .field-label > span { color: var(--primary); font-size: 9px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
|
|
80
|
+
.contact-section small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
81
|
+
|
|
82
|
+
.choices { display: grid; gap: 7px; }
|
|
83
|
+
.choice { display: flex; align-items: flex-start; gap: 8px; font-weight: 560; }
|
|
84
|
+
.choice input { margin-top: 2px; accent-color: var(--primary); }
|
|
85
|
+
.turnstile-mount { min-height: 0; overflow: hidden; }
|
|
86
|
+
.verification-status { margin: 8px 0 12px 35px; color: var(--muted); font-size: 10px; }
|
|
87
|
+
.verification-status.verified { color: var(--primary); font-weight: 700; }
|
|
88
|
+
.consent-card { display: flex; align-items: flex-start; gap: 11px; padding: 13px 14px; border: 1px solid var(--line); border-radius: calc(var(--radius) * .7); background: color-mix(in srgb, var(--page) 52%, var(--surface)); cursor: pointer; }
|
|
89
|
+
.consent-card input { width: 17px; height: 17px; margin: 1px 0 0; flex: 0 0 auto; accent-color: var(--primary); }
|
|
90
|
+
.consent-card span { display: grid; gap: 2px; }
|
|
91
|
+
.consent-card strong { font-size: 11px; }
|
|
92
|
+
.consent-card small { color: var(--muted); font-size: 9px; font-weight: 500; line-height: 1.45; }
|
|
93
|
+
|
|
94
|
+
.form-error { min-height: 18px; margin: 14px 0 0; color: #a33b34; font-size: 11px; font-weight: 650; line-height: 1.45; }
|
|
95
|
+
.form-actions { display: flex; align-items: center; justify-content: flex-end; gap: 9px; margin-top: 8px; }
|
|
96
|
+
button, .link-button { appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 9px; min-height: 42px; border: 0; border-radius: 999px; padding: 10px 17px; text-decoration: none; cursor: pointer; transition: transform 150ms ease, box-shadow 150ms ease, background 150ms ease; }
|
|
97
|
+
button:focus-visible, .link-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 25%, transparent); outline-offset: 3px; }
|
|
98
|
+
.primary { background: var(--primary); color: var(--on-primary); font-weight: 760; box-shadow: 0 8px 20px color-mix(in srgb, var(--primary) 20%, transparent); }
|
|
99
|
+
.primary:hover { transform: translateY(-1px); box-shadow: 0 10px 24px color-mix(in srgb, var(--primary) 26%, transparent); }
|
|
100
|
+
.primary:disabled { cursor: wait; opacity: .62; transform: none; box-shadow: none; }
|
|
101
|
+
.quiet, .secondary { background: transparent; color: var(--muted); font-weight: 680; }
|
|
102
|
+
.secondary { border: 1px solid var(--line); background: var(--surface); color: var(--text); }
|
|
103
|
+
.privacy-footer { margin: 14px 0 0; color: var(--muted); font-size: 9px; text-align: center; }
|
|
104
|
+
|
|
105
|
+
.state-view { display: grid; min-height: 390px; place-content: center; justify-items: center; padding: 42px 28px; text-align: center; }
|
|
106
|
+
.state-view p { max-width: 46ch; overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.55; }
|
|
107
|
+
.spinner { width: 30px; height: 30px; margin-bottom: 10px; border: 3px solid color-mix(in srgb, var(--primary) 16%, transparent); border-top-color: var(--primary); border-radius: 50%; animation: spin 800ms linear infinite; }
|
|
108
|
+
.state-icon, .success-icon { display: grid; place-items: center; width: 48px; height: 48px; margin-bottom: 14px; border-radius: 50%; font-size: 21px; font-weight: 850; }
|
|
109
|
+
.state-icon { background: #fff0ed; color: #a33b34; }
|
|
110
|
+
.success-icon { background: color-mix(in srgb, var(--primary) 10%, var(--surface)); color: var(--primary); }
|
|
111
|
+
.success-view h1 { max-width: 13ch; }
|
|
112
|
+
.success-view > p:not(.eyebrow):not(.notification-reminder) { margin: 13px 0 20px; }
|
|
113
|
+
.reference-card { display: flex; align-items: center; gap: 10px; width: min(100%, 430px); margin-bottom: 14px; padding: 11px 12px 11px 14px; border: 1px solid var(--line); border-radius: calc(var(--radius) * .7); background: color-mix(in srgb, var(--page) 56%, var(--surface)); text-align: left; }
|
|
114
|
+
.reference-card span { color: var(--muted); font-size: 9px; }
|
|
115
|
+
.reference-card strong { min-width: 0; flex: 1; overflow-wrap: anywhere; font-size: 11px; letter-spacing: .02em; }
|
|
116
|
+
.reference-card button { min-height: 30px; padding: 6px 10px; background: var(--surface); color: var(--primary); font-size: 9px; font-weight: 750; }
|
|
117
|
+
.notification-reminder { display: flex; align-items: flex-start; gap: 7px; margin: 1px 0 18px; color: var(--muted); font-size: 9px !important; text-align: left; }
|
|
118
|
+
.notification-reminder span { margin-top: 4px; color: var(--accent); font-size: 6px; }
|
|
119
|
+
.finish-button { width: min(100%, 430px); }
|
|
120
|
+
|
|
121
|
+
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
122
|
+
body.compact .card-header { padding-inline: 18px; }
|
|
123
|
+
body.compact form { padding: 22px 18px 20px; }
|
|
124
|
+
body.compact .reason-grid { grid-template-columns: 1fr; }
|
|
125
|
+
body.compact .privacy-mark { display: none; }
|
|
126
|
+
body.compact .form-actions { justify-content: stretch; }
|
|
127
|
+
body.compact .form-actions button { flex: 1; }
|
|
128
|
+
|
|
129
|
+
@keyframes spin { to { transform: rotate(1turn); } }
|
|
130
|
+
@media (max-width: 430px) {
|
|
131
|
+
.card-header { padding-inline: 18px; }
|
|
132
|
+
form { padding: 22px 18px 20px; }
|
|
133
|
+
.reason-grid { grid-template-columns: 1fr; }
|
|
134
|
+
.privacy-mark { display: none; }
|
|
135
|
+
.state-view { min-height: 340px; padding: 34px 18px; }
|
|
136
|
+
.reference-card { align-items: flex-start; flex-wrap: wrap; }
|
|
137
|
+
.reference-card strong { width: calc(100% - 80px); }
|
|
138
|
+
}
|
|
139
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } }
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
const MESSAGE_VERSION = 1;
|
|
2
|
+
const query = new URLSearchParams(location.search);
|
|
3
|
+
const integrationId = query.get("integration_id") || "";
|
|
4
|
+
const formVersion = query.get("form_version") || "general-v1";
|
|
5
|
+
const preview = query.get("preview") === "1";
|
|
6
|
+
let parentOrigin = "";
|
|
7
|
+
try { parentOrigin = new URL(query.get("parent_origin") || "").origin; } catch { parentOrigin = ""; }
|
|
8
|
+
|
|
9
|
+
const elements = Object.fromEntries([...document.querySelectorAll("[id]")].map((element) => [element.id.replaceAll("-", "_"), element]));
|
|
10
|
+
const state = {
|
|
11
|
+
config: null,
|
|
12
|
+
mode: "contextual",
|
|
13
|
+
contextToken: "",
|
|
14
|
+
target: null,
|
|
15
|
+
turnstileToken: "",
|
|
16
|
+
turnstileId: null,
|
|
17
|
+
submitting: false,
|
|
18
|
+
pendingSubmit: false,
|
|
19
|
+
verificationTimer: null,
|
|
20
|
+
idempotencyKey: crypto.randomUUID(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function post(message) {
|
|
24
|
+
if (!parentOrigin || parent === globalThis) return;
|
|
25
|
+
parent.postMessage({ version: MESSAGE_VERSION, ...message }, parentOrigin);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readable(value) {
|
|
29
|
+
return String(value || "").replaceAll("_", " ").replace(/\b\w/gu, (letter) => letter.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function initials(value) {
|
|
33
|
+
return String(value || "SR").split(/\s+/u).slice(0, 2).map((part) => part[0] || "").join("").toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyBrand(brand = {}) {
|
|
37
|
+
const variables = {
|
|
38
|
+
"--primary": brand.primary_color,
|
|
39
|
+
"--on-primary": brand.primary_contrast_color,
|
|
40
|
+
"--accent": brand.accent_color,
|
|
41
|
+
"--page": brand.background_color,
|
|
42
|
+
"--surface": brand.surface_color,
|
|
43
|
+
"--text": brand.text_color,
|
|
44
|
+
"--muted": brand.muted_color,
|
|
45
|
+
"--line": brand.border_color,
|
|
46
|
+
"--body": brand.body_font_family,
|
|
47
|
+
"--heading": brand.heading_font_family,
|
|
48
|
+
"--radius": Number.isInteger(brand.border_radius) ? `${brand.border_radius}px` : undefined,
|
|
49
|
+
};
|
|
50
|
+
for (const [key, value] of Object.entries(variables)) if (value) document.documentElement.style.setProperty(key, value);
|
|
51
|
+
const name = brand.organization_name || "Safest Resolve";
|
|
52
|
+
elements.brand_name.textContent = name;
|
|
53
|
+
elements.brand_fallback.textContent = initials(name);
|
|
54
|
+
elements.brand_logo.hidden = !brand.logo_url;
|
|
55
|
+
elements.brand_fallback.hidden = Boolean(brand.logo_url);
|
|
56
|
+
if (brand.logo_url) { elements.brand_logo.src = brand.logo_url; elements.brand_logo.alt = brand.logo_alt || `${name} logo`; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function option(value, label) {
|
|
60
|
+
const item = document.createElement("option");
|
|
61
|
+
item.value = value;
|
|
62
|
+
item.textContent = label;
|
|
63
|
+
return item;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fieldLabel(definition) {
|
|
67
|
+
const header = document.createElement("span");
|
|
68
|
+
header.className = "field-label";
|
|
69
|
+
const label = document.createElement("span");
|
|
70
|
+
label.textContent = definition.label || readable(definition.key);
|
|
71
|
+
header.append(label);
|
|
72
|
+
if (!definition.required) {
|
|
73
|
+
const optional = document.createElement("span");
|
|
74
|
+
optional.className = "optional";
|
|
75
|
+
optional.textContent = "Optional";
|
|
76
|
+
header.append(optional);
|
|
77
|
+
}
|
|
78
|
+
return header;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function answerField(definition) {
|
|
82
|
+
const key = String(definition.key);
|
|
83
|
+
const wrapper = document.createElement(definition.type === "multi_select" ? "fieldset" : "label");
|
|
84
|
+
wrapper.className = "answer-field";
|
|
85
|
+
wrapper.dataset.answerKey = key;
|
|
86
|
+
if (wrapper instanceof HTMLFieldSetElement) {
|
|
87
|
+
const legend = document.createElement("legend");
|
|
88
|
+
legend.append(fieldLabel(definition));
|
|
89
|
+
wrapper.append(legend);
|
|
90
|
+
} else wrapper.append(fieldLabel(definition));
|
|
91
|
+
if (definition.help_text) {
|
|
92
|
+
const help = document.createElement("small");
|
|
93
|
+
help.textContent = definition.help_text;
|
|
94
|
+
wrapper.append(help);
|
|
95
|
+
}
|
|
96
|
+
let control;
|
|
97
|
+
if (definition.type === "long_text") control = document.createElement("textarea");
|
|
98
|
+
else if (definition.type === "select" || definition.type === "boolean") {
|
|
99
|
+
control = document.createElement("select");
|
|
100
|
+
control.append(option("", "Choose an option"));
|
|
101
|
+
const choices = definition.type === "boolean"
|
|
102
|
+
? [{ value: "true", label: "Yes" }, { value: "false", label: "No" }]
|
|
103
|
+
: definition.options || [];
|
|
104
|
+
for (const choice of choices) control.append(option(choice.value, choice.label));
|
|
105
|
+
} else if (definition.type === "multi_select") {
|
|
106
|
+
control = document.createElement("div");
|
|
107
|
+
control.className = "choices";
|
|
108
|
+
for (const choice of definition.options || []) {
|
|
109
|
+
const label = document.createElement("label");
|
|
110
|
+
label.className = "choice";
|
|
111
|
+
const input = document.createElement("input");
|
|
112
|
+
input.type = "checkbox";
|
|
113
|
+
input.name = `answer-${key}`;
|
|
114
|
+
input.value = choice.value;
|
|
115
|
+
label.append(input, document.createTextNode(choice.label));
|
|
116
|
+
control.append(label);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
control = document.createElement("input");
|
|
120
|
+
control.type = definition.type === "number" ? "number" : definition.type === "url" ? "url" : "text";
|
|
121
|
+
}
|
|
122
|
+
control.dataset.answerControl = key;
|
|
123
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement) {
|
|
124
|
+
control.name = `answer-${key}`;
|
|
125
|
+
control.required = definition.required === true;
|
|
126
|
+
if (definition.placeholder) control.placeholder = definition.placeholder;
|
|
127
|
+
if (definition.max_length) control.maxLength = Number(definition.max_length);
|
|
128
|
+
if (definition.type === "number") {
|
|
129
|
+
if (definition.min_value !== undefined) control.min = String(definition.min_value);
|
|
130
|
+
if (definition.max_value !== undefined) control.max = String(definition.max_value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
wrapper.append(control);
|
|
134
|
+
return wrapper;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function collectAnswers() {
|
|
138
|
+
const answers = {};
|
|
139
|
+
for (const definition of state.config.form.fields || []) {
|
|
140
|
+
const key = String(definition.key);
|
|
141
|
+
const wrapper = elements.answer_fields.querySelector(`[data-answer-key="${CSS.escape(key)}"]`);
|
|
142
|
+
let value;
|
|
143
|
+
if (definition.type === "multi_select") value = [...wrapper.querySelectorAll("input:checked")].map((input) => input.value);
|
|
144
|
+
else {
|
|
145
|
+
const control = wrapper.querySelector("[data-answer-control]");
|
|
146
|
+
if (definition.type === "number") value = control.value === "" ? undefined : control.valueAsNumber;
|
|
147
|
+
else if (definition.type === "boolean") value = control.value === "" ? undefined : control.value === "true";
|
|
148
|
+
else value = control.value.trim();
|
|
149
|
+
}
|
|
150
|
+
const missing = value === undefined || value === "" || Array.isArray(value) && value.length === 0;
|
|
151
|
+
if (definition.required && missing) {
|
|
152
|
+
wrapper.querySelector("input, select, textarea")?.focus();
|
|
153
|
+
throw new Error(`${definition.label || readable(key)} is required.`);
|
|
154
|
+
}
|
|
155
|
+
if (!missing) answers[key] = value;
|
|
156
|
+
}
|
|
157
|
+
return answers;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function contextDigest(token) {
|
|
161
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
162
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function waitForTurnstile() {
|
|
166
|
+
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
167
|
+
if (typeof globalThis.turnstile?.render === "function") return globalThis.turnstile;
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
169
|
+
}
|
|
170
|
+
throw new Error("Human verification could not be loaded. Check your connection and try again.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function prepareTurnstile() {
|
|
174
|
+
if (preview || state.turnstileId !== null || !state.config) return;
|
|
175
|
+
if (state.mode === "contextual" && !state.contextToken) return;
|
|
176
|
+
const turnstile = await waitForTurnstile();
|
|
177
|
+
const options = {
|
|
178
|
+
sitekey: state.config.human_verification.site_key,
|
|
179
|
+
action: state.mode === "contextual" ? "widget_report_submit" : "anonymous_report_submit",
|
|
180
|
+
appearance: "interaction-only",
|
|
181
|
+
execution: "execute",
|
|
182
|
+
size: "flexible",
|
|
183
|
+
callback: (token) => {
|
|
184
|
+
clearTimeout(state.verificationTimer);
|
|
185
|
+
state.verificationTimer = null;
|
|
186
|
+
state.turnstileToken = token;
|
|
187
|
+
elements.verification_status.textContent = "Human verification complete.";
|
|
188
|
+
elements.verification_status.classList.add("verified");
|
|
189
|
+
if (state.pendingSubmit) void submitReport();
|
|
190
|
+
},
|
|
191
|
+
"expired-callback": () => {
|
|
192
|
+
clearTimeout(state.verificationTimer);
|
|
193
|
+
state.verificationTimer = null;
|
|
194
|
+
state.turnstileToken = "";
|
|
195
|
+
elements.verification_status.textContent = "Verification expired. It will run again when you submit.";
|
|
196
|
+
elements.verification_status.classList.remove("verified");
|
|
197
|
+
},
|
|
198
|
+
"error-callback": () => {
|
|
199
|
+
clearTimeout(state.verificationTimer);
|
|
200
|
+
state.verificationTimer = null;
|
|
201
|
+
state.turnstileToken = "";
|
|
202
|
+
state.pendingSubmit = false;
|
|
203
|
+
setBusy(false);
|
|
204
|
+
showFormError("Human verification could not finish. Check your connection and try again.");
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
if (state.mode === "contextual") options.cData = await contextDigest(state.contextToken);
|
|
208
|
+
state.turnstileId = turnstile.render(elements.turnstile_mount, options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resetTurnstile() {
|
|
212
|
+
clearTimeout(state.verificationTimer);
|
|
213
|
+
state.verificationTimer = null;
|
|
214
|
+
state.turnstileToken = "";
|
|
215
|
+
state.pendingSubmit = false;
|
|
216
|
+
elements.verification_status.textContent = "Human verification will run when you submit.";
|
|
217
|
+
elements.verification_status.classList.remove("verified");
|
|
218
|
+
if (state.turnstileId !== null && globalThis.turnstile?.reset) globalThis.turnstile.reset(state.turnstileId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setBusy(busy, label = "Sending report…") {
|
|
222
|
+
state.submitting = busy;
|
|
223
|
+
elements.submit_report.disabled = busy;
|
|
224
|
+
elements.cancel_report.disabled = busy;
|
|
225
|
+
elements.submit_report.querySelector("span").textContent = busy ? label : "Send report";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function showFormError(message) {
|
|
229
|
+
elements.form_error.textContent = message;
|
|
230
|
+
if (message) elements.form_error.focus();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderConfig(config) {
|
|
234
|
+
state.config = config;
|
|
235
|
+
state.mode = config.mode === "anonymous" ? "anonymous" : "contextual";
|
|
236
|
+
applyBrand(config.brand);
|
|
237
|
+
elements.form_title.textContent = config.form.title || "Report a safety concern";
|
|
238
|
+
elements.form_description.textContent = config.form.description || "Tell the safety team what happened.";
|
|
239
|
+
elements.consent_notice.textContent = config.form.consent_notice;
|
|
240
|
+
elements.mode_label.textContent = state.mode === "anonymous" ? "Anonymous report" : "Private report";
|
|
241
|
+
elements.trust_title.textContent = state.mode === "anonymous" ? "No account identity is attached" : "Right item, private answers";
|
|
242
|
+
elements.trust_copy.textContent = state.mode === "anonymous"
|
|
243
|
+
? "The service receives what you write and your contact email, but it does not claim the email proves who you are."
|
|
244
|
+
: "This app securely attached the selected item. Only authorized reviewers can read what you write.";
|
|
245
|
+
elements.contact_section.hidden = state.mode !== "anonymous";
|
|
246
|
+
elements.reporter_email.required = state.mode === "anonymous";
|
|
247
|
+
elements.reason_options.replaceChildren(...config.form.reasons.map((reason, index) => {
|
|
248
|
+
const label = document.createElement("label");
|
|
249
|
+
label.className = "reason-option";
|
|
250
|
+
const input = document.createElement("input");
|
|
251
|
+
input.type = "radio";
|
|
252
|
+
input.name = "reason";
|
|
253
|
+
input.value = reason.code;
|
|
254
|
+
input.required = true;
|
|
255
|
+
if (index === 0) input.checked = true;
|
|
256
|
+
const copy = document.createElement("span");
|
|
257
|
+
copy.textContent = reason.label;
|
|
258
|
+
label.append(input, copy);
|
|
259
|
+
return label;
|
|
260
|
+
}));
|
|
261
|
+
elements.answer_fields.replaceChildren(...(config.form.fields || []).map(answerField));
|
|
262
|
+
elements.loading_view.hidden = true;
|
|
263
|
+
elements.error_view.hidden = true;
|
|
264
|
+
elements.report_form.hidden = false;
|
|
265
|
+
if (preview) {
|
|
266
|
+
elements.submit_report.disabled = true;
|
|
267
|
+
elements.submit_report.querySelector("span").textContent = "Preview only";
|
|
268
|
+
elements.verification_status.textContent = "Human verification appears here in the live channel.";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function loadConfig() {
|
|
273
|
+
elements.loading_view.hidden = false;
|
|
274
|
+
elements.error_view.hidden = true;
|
|
275
|
+
elements.report_form.hidden = true;
|
|
276
|
+
try {
|
|
277
|
+
const url = new URL("/v1/widget/config", location.origin);
|
|
278
|
+
url.searchParams.set("integration_id", integrationId);
|
|
279
|
+
url.searchParams.set("form_version", formVersion);
|
|
280
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, credentials: "omit", referrerPolicy: "no-referrer" });
|
|
281
|
+
const result = await response.json().catch(() => ({}));
|
|
282
|
+
if (!response.ok) throw new Error(result?.error?.message || "This reporting channel is unavailable.");
|
|
283
|
+
renderConfig(result);
|
|
284
|
+
post({ type: "safest-resolve:ready", mode: state.mode });
|
|
285
|
+
} catch (error) {
|
|
286
|
+
elements.loading_view.hidden = true;
|
|
287
|
+
elements.error_view.hidden = false;
|
|
288
|
+
elements.setup_error.textContent = error instanceof Error ? error.message : "This reporting channel is unavailable.";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function submitReport() {
|
|
293
|
+
if (state.submitting && !state.pendingSubmit) return;
|
|
294
|
+
showFormError("");
|
|
295
|
+
let answers;
|
|
296
|
+
try {
|
|
297
|
+
if (!elements.report_form.reportValidity()) throw new Error("Complete the required fields before sending.");
|
|
298
|
+
answers = collectAnswers();
|
|
299
|
+
if (!elements.consent.checked) throw new Error("Confirm that you are ready to send this report.");
|
|
300
|
+
if (state.mode === "contextual" && !state.contextToken) throw new Error("The secure report connection is not ready yet.");
|
|
301
|
+
if (state.mode === "anonymous" && (!state.target?.type || !state.target?.reference)) throw new Error("The application did not provide the item being reported.");
|
|
302
|
+
} catch (error) {
|
|
303
|
+
showFormError(error instanceof Error ? error.message : "Complete the required fields before sending.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!state.turnstileToken) {
|
|
307
|
+
state.pendingSubmit = true;
|
|
308
|
+
setBusy(true, "Checking…");
|
|
309
|
+
elements.verification_status.textContent = "Checking this submission…";
|
|
310
|
+
try {
|
|
311
|
+
await prepareTurnstile();
|
|
312
|
+
state.verificationTimer = setTimeout(() => {
|
|
313
|
+
state.pendingSubmit = false;
|
|
314
|
+
setBusy(false);
|
|
315
|
+
resetTurnstile();
|
|
316
|
+
showFormError("Human verification took too long. Check your connection and try again.");
|
|
317
|
+
}, 15_000);
|
|
318
|
+
globalThis.turnstile.execute(state.turnstileId);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
state.pendingSubmit = false;
|
|
321
|
+
setBusy(false);
|
|
322
|
+
showFormError(error instanceof Error ? error.message : "Human verification could not start.");
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
state.pendingSubmit = false;
|
|
327
|
+
setBusy(true, "Sending report…");
|
|
328
|
+
const now = new Date().toISOString();
|
|
329
|
+
const body = {
|
|
330
|
+
schema_version: "1",
|
|
331
|
+
idempotency_key: state.idempotencyKey,
|
|
332
|
+
source: state.mode === "contextual" ? "signed_widget" : "public_page",
|
|
333
|
+
form_version: state.config.form.id,
|
|
334
|
+
reason_code: elements.report_form.elements.reason.value,
|
|
335
|
+
answers,
|
|
336
|
+
evidence: { mode: "reference_only", references: [] },
|
|
337
|
+
locale: document.documentElement.lang || "en",
|
|
338
|
+
consent: { notice: state.config.form.consent_notice, accepted_at: now },
|
|
339
|
+
submitted_at: now,
|
|
340
|
+
turnstile_token: state.turnstileToken,
|
|
341
|
+
};
|
|
342
|
+
let endpoint = "/v1/reports/anonymous";
|
|
343
|
+
if (state.mode === "contextual") {
|
|
344
|
+
endpoint = "/v1/reports";
|
|
345
|
+
body.context_token = state.contextToken;
|
|
346
|
+
} else {
|
|
347
|
+
body.integration_id = integrationId;
|
|
348
|
+
body.target = state.target;
|
|
349
|
+
body.reporter = { email: elements.reporter_email.value.trim() };
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetch(endpoint, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: { "content-type": "application/json" },
|
|
355
|
+
credentials: "omit",
|
|
356
|
+
referrerPolicy: "no-referrer",
|
|
357
|
+
body: JSON.stringify(body),
|
|
358
|
+
});
|
|
359
|
+
const result = await response.json().catch(() => ({}));
|
|
360
|
+
if (!response.ok) throw new Error(result?.error?.message || "The report could not be sent.");
|
|
361
|
+
elements.report_form.hidden = true;
|
|
362
|
+
elements.success_view.hidden = false;
|
|
363
|
+
elements.receipt_reference.textContent = result.reference || "";
|
|
364
|
+
elements.receipt_copy.textContent = state.mode === "anonymous"
|
|
365
|
+
? `We’ll send updates to ${elements.reporter_email.value.trim().toLowerCase()}. You can close this window now.`
|
|
366
|
+
: "You can close this window now. Updates will be sent through the application’s notification channel.";
|
|
367
|
+
elements.success_view.focus();
|
|
368
|
+
post({ type: "safest-resolve:submitted", reference: result.reference || "", mode: state.mode });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
setBusy(false);
|
|
371
|
+
resetTurnstile();
|
|
372
|
+
showFormError(error instanceof Error ? error.message : "The report could not be sent. Try again.");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
elements.report_form.addEventListener("submit", (event) => { event.preventDefault(); void submitReport(); });
|
|
377
|
+
elements.cancel_report.addEventListener("click", () => post({ type: "safest-resolve:cancel" }));
|
|
378
|
+
elements.finish_report.addEventListener("click", () => post({ type: "safest-resolve:cancel" }));
|
|
379
|
+
elements.retry_setup.addEventListener("click", () => void loadConfig());
|
|
380
|
+
elements.copy_reference.addEventListener("click", async () => {
|
|
381
|
+
try { await navigator.clipboard.writeText(elements.receipt_reference.textContent); elements.copy_reference.textContent = "Copied"; }
|
|
382
|
+
catch { elements.copy_reference.textContent = "Select to copy"; }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
globalThis.addEventListener("message", (event) => {
|
|
386
|
+
if (!parentOrigin || event.origin !== parentOrigin || event.source !== parent) return;
|
|
387
|
+
const message = event.data;
|
|
388
|
+
if (!message || typeof message !== "object" || message.version !== MESSAGE_VERSION || typeof message.type !== "string") return;
|
|
389
|
+
if (message.type === "safest-resolve:viewport") document.body.classList.toggle("compact", Number(message.width) < 480);
|
|
390
|
+
if (preview && message.type === "safest-resolve:preview" && message.config && typeof message.config === "object") {
|
|
391
|
+
renderConfig({ ...state.config, form: message.config, brand: message.brand && typeof message.brand === "object" ? message.brand : state.config?.brand });
|
|
392
|
+
}
|
|
393
|
+
if (message.type === "safest-resolve:context") {
|
|
394
|
+
if (state.mode !== "contextual" || typeof message.contextToken !== "string" || !message.contextToken) return;
|
|
395
|
+
state.contextToken = message.contextToken;
|
|
396
|
+
state.target = message.target && typeof message.target === "object" ? message.target : null;
|
|
397
|
+
elements.verification_status.textContent = "You’re reporting the right item. We’ll check for spam when you send.";
|
|
398
|
+
void prepareTurnstile().catch((error) => showFormError(error instanceof Error ? error.message : "Human verification could not load."));
|
|
399
|
+
}
|
|
400
|
+
if (message.type === "safest-resolve:target" && state.mode === "anonymous") {
|
|
401
|
+
state.target = message.target && typeof message.target === "object" ? message.target : null;
|
|
402
|
+
void prepareTurnstile().catch((error) => showFormError(error instanceof Error ? error.message : "Human verification could not load."));
|
|
403
|
+
}
|
|
404
|
+
if (message.type === "safest-resolve:context-error") showFormError(typeof message.message === "string" ? message.message : "Secure reporting context was unavailable.");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
new ResizeObserver(() => post({ type: "safest-resolve:resize", height: document.documentElement.scrollHeight })).observe(elements.report_card);
|
|
408
|
+
void loadConfig();
|