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,80 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
width: 100%;
|
|
4
|
+
max-width: 680px;
|
|
5
|
+
--se-frame-max-height: 820px;
|
|
6
|
+
height: var(--se-frame-height, auto);
|
|
7
|
+
color: #17211c;
|
|
8
|
+
font-family: Manrope, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
12
|
+
[hidden] { display: none !important; }
|
|
13
|
+
|
|
14
|
+
.frame-shell {
|
|
15
|
+
position: relative;
|
|
16
|
+
width: 100%;
|
|
17
|
+
min-height: min(420px, var(--se-frame-max-height));
|
|
18
|
+
max-height: var(--se-frame-max-height);
|
|
19
|
+
height: var(--se-frame-height, auto);
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
border: 1px solid color-mix(in srgb, var(--se-primary, #173f35) 16%, #dce4df);
|
|
22
|
+
border-radius: var(--se-radius, 22px);
|
|
23
|
+
background: var(--se-surface, #fff);
|
|
24
|
+
box-shadow: 0 24px 70px rgba(20, 48, 39, .14), 0 2px 10px rgba(20, 48, 39, .06);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
iframe {
|
|
28
|
+
display: block;
|
|
29
|
+
width: 100%;
|
|
30
|
+
min-height: min(420px, var(--se-frame-max-height));
|
|
31
|
+
max-height: var(--se-frame-max-height);
|
|
32
|
+
height: var(--se-frame-height, auto);
|
|
33
|
+
border: 0;
|
|
34
|
+
background: transparent;
|
|
35
|
+
transition: height 180ms ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.loading, .error {
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: 0;
|
|
41
|
+
z-index: 2;
|
|
42
|
+
display: grid;
|
|
43
|
+
place-content: center;
|
|
44
|
+
justify-items: center;
|
|
45
|
+
gap: 14px;
|
|
46
|
+
padding: 32px;
|
|
47
|
+
background:
|
|
48
|
+
radial-gradient(circle at 50% 25%, color-mix(in srgb, var(--se-primary, #173f35) 9%, transparent), transparent 38%),
|
|
49
|
+
var(--se-surface, #fff);
|
|
50
|
+
color: var(--se-muted, #65726b);
|
|
51
|
+
text-align: center;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.loading-mark {
|
|
56
|
+
width: 34px;
|
|
57
|
+
aspect-ratio: 1;
|
|
58
|
+
border: 3px solid color-mix(in srgb, var(--se-primary, #173f35) 18%, transparent);
|
|
59
|
+
border-top-color: var(--se-primary, #173f35);
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
animation: spin 800ms linear infinite;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.error strong { color: var(--se-text, #17211c); font-size: 17px; }
|
|
65
|
+
.error span { max-width: 36ch; line-height: 1.55; }
|
|
66
|
+
.error button {
|
|
67
|
+
appearance: none;
|
|
68
|
+
border: 0;
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
padding: 11px 18px;
|
|
71
|
+
background: var(--se-primary, #173f35);
|
|
72
|
+
color: var(--se-primary-contrast, #fff);
|
|
73
|
+
font: inherit;
|
|
74
|
+
font-weight: 750;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
.error button:focus-visible { outline: 3px solid color-mix(in srgb, var(--se-primary, #173f35) 32%, transparent); outline-offset: 3px; }
|
|
78
|
+
|
|
79
|
+
@keyframes spin { to { transform: rotate(1turn); } }
|
|
80
|
+
@media (prefers-reduced-motion: reduce) { .loading-mark { animation-duration: 1600ms; } iframe { transition: none; } }
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const MESSAGE_VERSION = 1;
|
|
2
|
+
const MIN_HEIGHT = 420;
|
|
3
|
+
const MAX_HEIGHT = 820;
|
|
4
|
+
|
|
5
|
+
const template = document.createElement("template");
|
|
6
|
+
template.innerHTML = `
|
|
7
|
+
<section class="frame-shell" aria-busy="true">
|
|
8
|
+
<div class="loading" role="status" aria-live="polite">
|
|
9
|
+
<span class="loading-mark" aria-hidden="true"></span>
|
|
10
|
+
<span>Preparing a secure report…</span>
|
|
11
|
+
</div>
|
|
12
|
+
<iframe title="Report a safety concern" referrerpolicy="no-referrer" allow=""></iframe>
|
|
13
|
+
<div class="error" role="alert" hidden>
|
|
14
|
+
<strong>The report form could not open</strong>
|
|
15
|
+
<span></span>
|
|
16
|
+
<button type="button">Try again</button>
|
|
17
|
+
</div>
|
|
18
|
+
</section>`;
|
|
19
|
+
|
|
20
|
+
function normalizedOrigin(value) {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value, location.href);
|
|
23
|
+
const supplied = value.replace(/\/$/u, "");
|
|
24
|
+
return value.startsWith("/") || url.origin === supplied ? url.origin : "";
|
|
25
|
+
} catch { return ""; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function positiveHeight(value) {
|
|
29
|
+
const height = Number(value);
|
|
30
|
+
return Number.isFinite(height) ? Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, Math.ceil(height))) : MIN_HEIGHT;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class SafestReport extends HTMLElement {
|
|
34
|
+
static observedAttributes = ["api-base", "integration-id", "form-version", "context-endpoint", "target-type", "target-reference", "preview"];
|
|
35
|
+
|
|
36
|
+
#contextToken = "";
|
|
37
|
+
#previewBrand = null;
|
|
38
|
+
#previewConfig = null;
|
|
39
|
+
#iframe = null;
|
|
40
|
+
#rootNode = null;
|
|
41
|
+
#loaded = false;
|
|
42
|
+
#loadingContext = false;
|
|
43
|
+
#messageHandler = null;
|
|
44
|
+
#resizeHandler = null;
|
|
45
|
+
|
|
46
|
+
connectedCallback() {
|
|
47
|
+
if (!this.#rootNode) {
|
|
48
|
+
const root = this.attachShadow({ mode: "closed" });
|
|
49
|
+
this.#rootNode = root;
|
|
50
|
+
const stylesheet = document.createElement("link");
|
|
51
|
+
stylesheet.rel = "stylesheet";
|
|
52
|
+
stylesheet.href = `${this.apiBase}/widget.css`;
|
|
53
|
+
stylesheet.referrerPolicy = "no-referrer";
|
|
54
|
+
root.append(stylesheet, template.content.cloneNode(true));
|
|
55
|
+
this.#iframe = root.querySelector("iframe");
|
|
56
|
+
root.querySelector(".error button").addEventListener("click", () => this.#mount());
|
|
57
|
+
this.#messageHandler = (event) => this.#onMessage(event);
|
|
58
|
+
this.#resizeHandler = () => this.#send({ type: "safest-resolve:viewport", width: this.getBoundingClientRect().width });
|
|
59
|
+
}
|
|
60
|
+
globalThis.addEventListener("message", this.#messageHandler);
|
|
61
|
+
globalThis.addEventListener("resize", this.#resizeHandler, { passive: true });
|
|
62
|
+
this.#mount();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disconnectedCallback() {
|
|
66
|
+
if (this.#messageHandler) globalThis.removeEventListener("message", this.#messageHandler);
|
|
67
|
+
if (this.#resizeHandler) globalThis.removeEventListener("resize", this.#resizeHandler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
attributeChangedCallback() {
|
|
71
|
+
if (this.isConnected && this.#loaded) this.#mount();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get apiBase() { return (this.getAttribute("api-base") || location.origin).replace(/\/$/u, ""); }
|
|
75
|
+
get integrationId() { return this.getAttribute("integration-id") || ""; }
|
|
76
|
+
get formVersion() { return this.getAttribute("form-version") || "general-v1"; }
|
|
77
|
+
get contextEndpoint() { return this.getAttribute("context-endpoint") || ""; }
|
|
78
|
+
get targetType() { return this.getAttribute("target-type") || ""; }
|
|
79
|
+
get targetReference() { return this.getAttribute("target-reference") || ""; }
|
|
80
|
+
get preview() { return this.hasAttribute("preview"); }
|
|
81
|
+
set preview(value) {
|
|
82
|
+
const enabled = value !== false && value !== null && value !== undefined && value !== "false";
|
|
83
|
+
if (enabled !== this.hasAttribute("preview")) this.toggleAttribute("preview", enabled);
|
|
84
|
+
}
|
|
85
|
+
get previewBrand() { return this.#previewBrand; }
|
|
86
|
+
set previewBrand(value) {
|
|
87
|
+
this.#previewBrand = value && typeof value === "object" ? value : null;
|
|
88
|
+
this.#sendPreview();
|
|
89
|
+
}
|
|
90
|
+
get previewConfig() { return this.#previewConfig; }
|
|
91
|
+
set previewConfig(value) {
|
|
92
|
+
this.#previewConfig = value && typeof value === "object" ? value : null;
|
|
93
|
+
this.#sendPreview();
|
|
94
|
+
}
|
|
95
|
+
get contextToken() { return this.#contextToken; }
|
|
96
|
+
set contextToken(value) {
|
|
97
|
+
this.#contextToken = typeof value === "string" ? value : "";
|
|
98
|
+
if (this.hasAttribute("context-token")) this.removeAttribute("context-token");
|
|
99
|
+
if (this.#loaded && this.#contextToken) this.#sendContext(this.#contextToken);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#root() { return this.#rootNode; }
|
|
103
|
+
|
|
104
|
+
#showError(message) {
|
|
105
|
+
const root = this.#root();
|
|
106
|
+
if (!root) return;
|
|
107
|
+
root.querySelector(".frame-shell").setAttribute("aria-busy", "false");
|
|
108
|
+
root.querySelector(".loading").hidden = true;
|
|
109
|
+
root.querySelector("iframe").hidden = true;
|
|
110
|
+
const error = root.querySelector(".error");
|
|
111
|
+
error.hidden = false;
|
|
112
|
+
error.querySelector("span").textContent = message;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#mount() {
|
|
116
|
+
const root = this.#root();
|
|
117
|
+
if (!root || !this.#iframe) return;
|
|
118
|
+
this.#loaded = false;
|
|
119
|
+
this.#loadingContext = false;
|
|
120
|
+
const apiOrigin = normalizedOrigin(this.apiBase);
|
|
121
|
+
if (!apiOrigin || !this.integrationId) {
|
|
122
|
+
this.#showError("The reporting channel is not configured correctly.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
root.querySelector(".frame-shell").setAttribute("aria-busy", "true");
|
|
126
|
+
root.querySelector(".loading").hidden = false;
|
|
127
|
+
root.querySelector(".error").hidden = true;
|
|
128
|
+
this.#iframe.hidden = false;
|
|
129
|
+
this.#iframe.style.height = `${MIN_HEIGHT}px`;
|
|
130
|
+
const url = new URL("/embed/", apiOrigin);
|
|
131
|
+
url.searchParams.set("integration_id", this.integrationId);
|
|
132
|
+
url.searchParams.set("form_version", this.formVersion);
|
|
133
|
+
url.searchParams.set("parent_origin", location.origin);
|
|
134
|
+
if (this.preview) url.searchParams.set("preview", "1");
|
|
135
|
+
this.#iframe.src = url.href;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#send(message) {
|
|
139
|
+
const apiOrigin = normalizedOrigin(this.apiBase);
|
|
140
|
+
if (!apiOrigin || !this.#iframe?.contentWindow) return;
|
|
141
|
+
this.#iframe.contentWindow.postMessage({ version: MESSAGE_VERSION, ...message }, apiOrigin);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#targetPayload() { return { type: this.targetType, reference: this.targetReference }; }
|
|
145
|
+
|
|
146
|
+
#sendContext(token) {
|
|
147
|
+
this.#send({ type: "safest-resolve:context", contextToken: token, target: this.#targetPayload() });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#sendPreview() {
|
|
151
|
+
if (!this.preview || !this.#loaded || !this.#previewConfig) return;
|
|
152
|
+
this.#send({ type: "safest-resolve:preview", config: this.#previewConfig, brand: this.#previewBrand });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #prepareContext() {
|
|
156
|
+
if (this.#loadingContext) return;
|
|
157
|
+
if (this.#contextToken) { this.#sendContext(this.#contextToken); return; }
|
|
158
|
+
if (!this.contextEndpoint) {
|
|
159
|
+
this.#send({ type: "safest-resolve:context-error", message: "This application has not connected its secure reporting endpoint." });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.#loadingContext = true;
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(this.contextEndpoint, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
167
|
+
credentials: "same-origin",
|
|
168
|
+
referrerPolicy: "same-origin",
|
|
169
|
+
body: JSON.stringify({ target: this.#targetPayload(), form_version: this.formVersion }),
|
|
170
|
+
});
|
|
171
|
+
const result = await response.json().catch(() => ({}));
|
|
172
|
+
if (!response.ok) throw new Error(result?.error?.message || result?.message || "Secure reporting context was unavailable.");
|
|
173
|
+
const token = result.context_token || result.contextToken;
|
|
174
|
+
if (typeof token !== "string" || !token) throw new Error("The application returned an invalid reporting context.");
|
|
175
|
+
this.#contextToken = token;
|
|
176
|
+
this.#sendContext(token);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.#send({ type: "safest-resolve:context-error", message: error instanceof Error ? error.message : "Secure reporting context was unavailable." });
|
|
179
|
+
} finally {
|
|
180
|
+
this.#loadingContext = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#onMessage(event) {
|
|
185
|
+
const apiOrigin = normalizedOrigin(this.apiBase);
|
|
186
|
+
if (!apiOrigin || event.origin !== apiOrigin || event.source !== this.#iframe?.contentWindow) return;
|
|
187
|
+
const message = event.data;
|
|
188
|
+
if (!message || typeof message !== "object" || message.version !== MESSAGE_VERSION || typeof message.type !== "string") return;
|
|
189
|
+
if (message.type === "safest-resolve:ready") {
|
|
190
|
+
this.#loaded = true;
|
|
191
|
+
const root = this.#root();
|
|
192
|
+
if (root) {
|
|
193
|
+
root.querySelector(".frame-shell").setAttribute("aria-busy", "false");
|
|
194
|
+
root.querySelector(".loading").hidden = true;
|
|
195
|
+
}
|
|
196
|
+
this.#send({ type: "safest-resolve:viewport", width: this.getBoundingClientRect().width });
|
|
197
|
+
if (this.preview) this.#sendPreview();
|
|
198
|
+
else if (message.mode === "contextual") void this.#prepareContext();
|
|
199
|
+
else this.#send({ type: "safest-resolve:target", target: this.#targetPayload() });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (message.type === "safest-resolve:resize") {
|
|
203
|
+
this.#iframe.style.height = `${positiveHeight(message.height)}px`;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (message.type === "safest-resolve:submitted") {
|
|
207
|
+
this.#contextToken = "";
|
|
208
|
+
this.dispatchEvent(new CustomEvent("safest-report-submitted", {
|
|
209
|
+
bubbles: true,
|
|
210
|
+
detail: { reference: typeof message.reference === "string" ? message.reference : "", mode: message.mode },
|
|
211
|
+
}));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (message.type === "safest-resolve:cancel") {
|
|
215
|
+
this.dispatchEvent(new CustomEvent("safest-report-cancelled", { bubbles: true }));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!customElements.get("safest-report")) customElements.define("safest-report", SafestReport);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./reports.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"installationId": "acme-resolve",
|
|
5
|
+
"resources": {
|
|
6
|
+
"workerName": "acme-resolve-worker",
|
|
7
|
+
"databaseName": "acme-resolve-db",
|
|
8
|
+
"profileMediaBucketName": "acme-resolve-profile-media",
|
|
9
|
+
"workflowArtifactsBucketName": "acme-resolve-workflow-artifacts",
|
|
10
|
+
"evidenceBucketName": "acme-resolve-evidence",
|
|
11
|
+
"exportsBucketName": "acme-resolve-exports",
|
|
12
|
+
"workflowName": "acme-resolve-workflows",
|
|
13
|
+
"reportQueueName": "acme-resolve-report-jobs",
|
|
14
|
+
"deliveryQueueName": "acme-resolve-delivery-jobs",
|
|
15
|
+
"operationsDlqName": "acme-resolve-operations-dlq"
|
|
16
|
+
},
|
|
17
|
+
"publicBaseUrl": "https://reports.example.com",
|
|
18
|
+
"allowedOrigins": [
|
|
19
|
+
"https://app.example.com"
|
|
20
|
+
],
|
|
21
|
+
"access": {
|
|
22
|
+
"audience": "replace-with-the-cloudflare-access-application-aud",
|
|
23
|
+
"ownerEmails": ["infrastructure@example.com"]
|
|
24
|
+
},
|
|
25
|
+
"email": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"fromAddress": "reports@example.com"
|
|
28
|
+
},
|
|
29
|
+
"auth": {
|
|
30
|
+
"password": true,
|
|
31
|
+
"providers": []
|
|
32
|
+
},
|
|
33
|
+
"retention": {
|
|
34
|
+
"reportsDays": 365,
|
|
35
|
+
"messagesDays": 180,
|
|
36
|
+
"appealWindowDays": 30
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://safest.tools/schemas/reports-installation-v1.json",
|
|
4
|
+
"title": "Safest Resolve installation",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "installationId", "resources", "publicBaseUrl", "allowedOrigins", "access", "email", "auth", "retention"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"installationId": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" },
|
|
12
|
+
"resources": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["workerName", "databaseName", "profileMediaBucketName", "workflowArtifactsBucketName", "evidenceBucketName", "exportsBucketName", "workflowName", "reportQueueName", "deliveryQueueName", "operationsDlqName"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"workerName": { "$ref": "#/$defs/resourceName" },
|
|
18
|
+
"databaseName": { "$ref": "#/$defs/resourceName" },
|
|
19
|
+
"profileMediaBucketName": { "$ref": "#/$defs/resourceName" },
|
|
20
|
+
"workflowArtifactsBucketName": { "$ref": "#/$defs/resourceName" },
|
|
21
|
+
"evidenceBucketName": { "$ref": "#/$defs/resourceName" },
|
|
22
|
+
"exportsBucketName": { "$ref": "#/$defs/resourceName" },
|
|
23
|
+
"workflowName": { "$ref": "#/$defs/resourceName" },
|
|
24
|
+
"reportQueueName": { "$ref": "#/$defs/resourceName" },
|
|
25
|
+
"deliveryQueueName": { "$ref": "#/$defs/resourceName" },
|
|
26
|
+
"operationsDlqName": { "$ref": "#/$defs/resourceName" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publicBaseUrl": { "type": "string", "format": "uri", "pattern": "^https://" },
|
|
30
|
+
"allowedOrigins": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"minItems": 1,
|
|
33
|
+
"maxItems": 20,
|
|
34
|
+
"uniqueItems": true,
|
|
35
|
+
"items": { "type": "string", "format": "uri", "pattern": "^https://" }
|
|
36
|
+
},
|
|
37
|
+
"access": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"required": ["audience", "ownerEmails"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"audience": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
43
|
+
"ownerEmails": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"maxItems": 20,
|
|
47
|
+
"uniqueItems": true,
|
|
48
|
+
"items": { "type": "string", "format": "email" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"email": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"required": ["enabled", "fromAddress"],
|
|
56
|
+
"properties": {
|
|
57
|
+
"enabled": { "type": "boolean" },
|
|
58
|
+
"fromAddress": { "type": "string", "maxLength": 254 }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"auth": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"required": ["password", "providers"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"password": { "const": true },
|
|
67
|
+
"providers": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"maxItems": 3,
|
|
70
|
+
"uniqueItems": true,
|
|
71
|
+
"items": { "enum": ["google", "github", "cloudflare"] }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"retention": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"additionalProperties": false,
|
|
78
|
+
"required": ["reportsDays", "messagesDays", "appealWindowDays"],
|
|
79
|
+
"properties": {
|
|
80
|
+
"reportsDays": { "type": "integer", "minimum": 30, "maximum": 3650 },
|
|
81
|
+
"messagesDays": { "type": "integer", "minimum": 30, "maximum": 3650 },
|
|
82
|
+
"appealWindowDays": { "type": "integer", "minimum": 1, "maximum": 365 }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"$defs": {
|
|
87
|
+
"resourceName": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { readSecrets, updateSecrets } from "./reports-secrets.mjs";
|
|
5
|
+
|
|
6
|
+
const OAUTH_PROVIDERS = Object.freeze([
|
|
7
|
+
{
|
|
8
|
+
id: "google",
|
|
9
|
+
name: "Google",
|
|
10
|
+
clientIdSecret: "AUTH_GOOGLE_CLIENT_ID",
|
|
11
|
+
clientSecretSecret: "AUTH_GOOGLE_CLIENT_SECRET",
|
|
12
|
+
dashboardUrl: "https://console.cloud.google.com/auth/clients",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "github",
|
|
16
|
+
name: "GitHub",
|
|
17
|
+
clientIdSecret: "AUTH_GITHUB_CLIENT_ID",
|
|
18
|
+
clientSecretSecret: "AUTH_GITHUB_CLIENT_SECRET",
|
|
19
|
+
dashboardUrl: "https://github.com/settings/developers",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "cloudflare",
|
|
23
|
+
name: "Cloudflare",
|
|
24
|
+
clientIdSecret: "AUTH_CLOUDFLARE_CLIENT_ID",
|
|
25
|
+
clientSecretSecret: "AUTH_CLOUDFLARE_CLIENT_SECRET",
|
|
26
|
+
dashboardUrl: "https://dash.cloudflare.com/?to=/:account/oauth-clients",
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function oauthCallbackUrl(publicBaseUrl, providerId) {
|
|
31
|
+
return `${new URL(publicBaseUrl).origin}/api/auth/callback/${providerId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requiredOAuthSecrets(config) {
|
|
35
|
+
const selected = new Set(config.auth?.providers ?? []);
|
|
36
|
+
return OAUTH_PROVIDERS.filter((provider) => selected.has(provider.id))
|
|
37
|
+
.flatMap((provider) => [provider.clientIdSecret, provider.clientSecretSecret]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setupInstructions(provider, publicBaseUrl) {
|
|
41
|
+
const callback = oauthCallbackUrl(publicBaseUrl, provider.id);
|
|
42
|
+
const shared = `Open: ${provider.dashboardUrl}\nCallback URL (copy exactly): ${callback}`;
|
|
43
|
+
if (provider.id === "google") {
|
|
44
|
+
return `${shared}\n\n1. Select or create a Google Cloud project.\n2. Configure the OAuth consent screen.\n3. Create a client with application type Web application.\n4. Add ${publicBaseUrl} as an Authorized JavaScript origin.\n5. Add the exact callback URL above as an Authorized redirect URI.\n6. Copy the Client ID and Client secret. Do not download or commit the JSON file.`;
|
|
45
|
+
}
|
|
46
|
+
if (provider.id === "github") {
|
|
47
|
+
return `${shared}\n\n1. Choose OAuth Apps, then New OAuth App.\n2. Set Homepage URL to ${publicBaseUrl}.\n3. Set Authorization callback URL to the exact callback above.\n4. Leave wildcard callback matching disabled.\n5. Register the app, generate a client secret, and copy both values.`;
|
|
48
|
+
}
|
|
49
|
+
return `${shared}\n\n1. Select the Cloudflare account, then Create client.\n2. Choose Authorization Code, response type code, and client_secret_basic.\n3. Add the exact callback URL above.\n4. Select only the User Details Read scope (user-details.read).\n5. Keep it private for members of this Cloudflare account, or complete domain verification before making it public for invited users from other accounts.\n6. Create the client and copy the secret now; Cloudflare will not show it again.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cancelled(value) {
|
|
53
|
+
return clack.isCancel(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function providerConfigured(provider, selected, secrets) {
|
|
57
|
+
return selected.has(provider.id) && Boolean(secrets[provider.clientIdSecret] && secrets[provider.clientSecretSecret]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function confirmEmailSending(config, ui = clack) {
|
|
61
|
+
const sender = config.email?.fromAddress ?? "";
|
|
62
|
+
const domain = sender.includes("@") ? sender.slice(sender.lastIndexOf("@") + 1) : sender;
|
|
63
|
+
ui.note(
|
|
64
|
+
`Sender: ${sender}\nDomain: ${domain}\n\n` +
|
|
65
|
+
`1. In Cloudflare, open Compute → Email Service → Email Sending.\n` +
|
|
66
|
+
`2. Onboard ${domain} and wait for its sending DNS records to show configured.\n` +
|
|
67
|
+
`3. Return here. Domain onboarding is required to email arbitrary invited users.`,
|
|
68
|
+
"Email delivery",
|
|
69
|
+
);
|
|
70
|
+
const action = await ui.select({
|
|
71
|
+
message: "Is this domain enabled for Cloudflare Email Sending?",
|
|
72
|
+
options: [
|
|
73
|
+
{ value: "ready", label: "Yes, continue" },
|
|
74
|
+
{ value: "back", label: "Back", hint: "stop without saving auth or provisioning resources" },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
return !cancelled(action) && action === "ready";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function authenticationWizard(config, existingSecrets, ui = clack) {
|
|
81
|
+
const secrets = { ...existingSecrets };
|
|
82
|
+
const selected = new Set(config.auth?.providers ?? []);
|
|
83
|
+
ui.intro("Safest Resolve authentication");
|
|
84
|
+
ui.note(
|
|
85
|
+
"Password login and email-based password recovery are always enabled. Add any OAuth providers you want normal invited users to see.",
|
|
86
|
+
"Sign-in methods",
|
|
87
|
+
);
|
|
88
|
+
while (true) {
|
|
89
|
+
const choice = await ui.select({
|
|
90
|
+
message: "Choose an OAuth provider to configure",
|
|
91
|
+
options: [
|
|
92
|
+
...OAUTH_PROVIDERS.map((provider) => ({
|
|
93
|
+
value: provider.id,
|
|
94
|
+
label: provider.name,
|
|
95
|
+
hint: providerConfigured(provider, selected, secrets) ? "configured" : "not configured",
|
|
96
|
+
})),
|
|
97
|
+
{ value: "done", label: "Continue", hint: "save these choices" },
|
|
98
|
+
{ value: "back", label: "Back", hint: "discard OAuth changes and stop setup" },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
if (cancelled(choice) || choice === "back") {
|
|
102
|
+
ui.cancel("No OAuth changes were saved.");
|
|
103
|
+
return { cancelled: true, config, secrets: existingSecrets };
|
|
104
|
+
}
|
|
105
|
+
if (choice === "done") {
|
|
106
|
+
const incomplete = OAUTH_PROVIDERS.find((provider) => selected.has(provider.id) &&
|
|
107
|
+
(!secrets[provider.clientIdSecret] || !secrets[provider.clientSecretSecret]));
|
|
108
|
+
if (incomplete) {
|
|
109
|
+
ui.note(`Finish or remove ${incomplete.name} before continuing.`, "Credentials missing");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const nextConfig = {
|
|
113
|
+
...config,
|
|
114
|
+
auth: { password: true, providers: OAUTH_PROVIDERS.map(({ id }) => id).filter((id) => selected.has(id)) },
|
|
115
|
+
};
|
|
116
|
+
ui.outro(selected.size ? `OAuth ready: ${[...selected].join(", ")}` : "Password-only sign-in selected.");
|
|
117
|
+
return { cancelled: false, config: nextConfig, secrets };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const provider = OAUTH_PROVIDERS.find(({ id }) => id === choice);
|
|
121
|
+
if (!provider) continue;
|
|
122
|
+
ui.note(setupInstructions(provider, config.publicBaseUrl), `${provider.name} OAuth setup`);
|
|
123
|
+
const action = await ui.select({
|
|
124
|
+
message: `${provider.name} setup`,
|
|
125
|
+
options: [
|
|
126
|
+
{ value: "configure", label: providerConfigured(provider, selected, secrets) ? "Replace credentials" : "Enter credentials" },
|
|
127
|
+
...(selected.has(provider.id) ? [{ value: "remove", label: `Remove ${provider.name}` }] : []),
|
|
128
|
+
{ value: "back", label: "Back", hint: "return without changing this provider" },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
if (cancelled(action) || action === "back") continue;
|
|
132
|
+
if (action === "remove") {
|
|
133
|
+
selected.delete(provider.id);
|
|
134
|
+
delete secrets[provider.clientIdSecret];
|
|
135
|
+
delete secrets[provider.clientSecretSecret];
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const clientId = await ui.text({
|
|
139
|
+
message: `${provider.name} Client ID`,
|
|
140
|
+
validate: (value) => value.trim() ? undefined : "Client ID is required",
|
|
141
|
+
});
|
|
142
|
+
if (cancelled(clientId)) continue;
|
|
143
|
+
const clientSecret = await ui.password({
|
|
144
|
+
message: `${provider.name} Client secret (input is masked)`,
|
|
145
|
+
validate: (value) => value.trim() ? undefined : "Client secret is required",
|
|
146
|
+
});
|
|
147
|
+
if (cancelled(clientSecret)) continue;
|
|
148
|
+
const save = await ui.select({
|
|
149
|
+
message: `Save ${provider.name} credentials to the owner-only local secrets file?`,
|
|
150
|
+
options: [
|
|
151
|
+
{ value: "save", label: "Save provider" },
|
|
152
|
+
{ value: "back", label: "Back", hint: "discard the values just entered" },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
if (cancelled(save) || save === "back") continue;
|
|
156
|
+
secrets[provider.clientIdSecret] = clientId.trim();
|
|
157
|
+
secrets[provider.clientSecretSecret] = clientSecret.trim();
|
|
158
|
+
selected.add(provider.id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function persistAuthenticationConfiguration({ configPath, secretsPath, config, secrets }) {
|
|
163
|
+
const absoluteConfig = resolve(configPath);
|
|
164
|
+
await writeFile(absoluteConfig, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
165
|
+
await updateSecrets(secretsPath, secrets);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function configureAuthentication({ configPath, secretsPath, config, interactive, ui = clack }) {
|
|
169
|
+
const secrets = await readSecrets(secretsPath);
|
|
170
|
+
if (!interactive) return { cancelled: false, config, secrets };
|
|
171
|
+
if (!await confirmEmailSending(config, ui)) {
|
|
172
|
+
ui.cancel("Setup stopped before provisioning. No OAuth changes were saved.");
|
|
173
|
+
return { cancelled: true, config, secrets };
|
|
174
|
+
}
|
|
175
|
+
const result = await authenticationWizard(config, secrets, ui);
|
|
176
|
+
if (!result.cancelled) {
|
|
177
|
+
await persistAuthenticationConfiguration({ configPath, secretsPath, config: result.config, secrets: result.secrets });
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|