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,292 @@
|
|
|
1
|
+
import { createRemoteJWKSet, decodeJwt, jwtVerify, type JWTPayload } from "jose";
|
|
2
|
+
import { ApiError } from "./report-http";
|
|
3
|
+
import { secretsEqual, sha256 } from "./report-crypto";
|
|
4
|
+
import {
|
|
5
|
+
permissionsForRoles,
|
|
6
|
+
resolveBetterAuthOperator,
|
|
7
|
+
resolveOperatorAccountSession,
|
|
8
|
+
verifyOperatorCsrf,
|
|
9
|
+
} from "./report-operator-accounts";
|
|
10
|
+
import {
|
|
11
|
+
appendBetterAuthCsrfCookie,
|
|
12
|
+
betterAuthCsrfHash,
|
|
13
|
+
betterAuthCsrfToken,
|
|
14
|
+
invitationIdFromRequest,
|
|
15
|
+
resolveBetterAuthSession,
|
|
16
|
+
} from "./report-better-auth";
|
|
17
|
+
import { loadIntegration } from "./report-repository";
|
|
18
|
+
import type { OperatorPermissions, OperatorSession } from "./report-types";
|
|
19
|
+
|
|
20
|
+
type AccessIdentity = JWTPayload & {
|
|
21
|
+
email?: unknown;
|
|
22
|
+
name?: unknown;
|
|
23
|
+
picture?: unknown;
|
|
24
|
+
user_uuid?: unknown;
|
|
25
|
+
groups?: unknown;
|
|
26
|
+
custom?: unknown;
|
|
27
|
+
};
|
|
28
|
+
type AccessContext = Pick<ExecutionContext, "access">;
|
|
29
|
+
|
|
30
|
+
const accessKeySets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
31
|
+
function bounded(value: unknown, maximum: number): string | undefined {
|
|
32
|
+
if (typeof value !== "string") return undefined;
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
return normalized ? normalized.slice(0, maximum) : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function avatarUrl(value: unknown): string | undefined {
|
|
38
|
+
const candidate = bounded(value, 2_048);
|
|
39
|
+
if (!candidate) return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(candidate);
|
|
42
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : undefined;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function issuer(value: unknown): string | null {
|
|
49
|
+
if (typeof value !== "string") return null;
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(value);
|
|
52
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.port
|
|
53
|
+
&& (url.pathname === "/" || url.pathname === "") && !url.search && !url.hash
|
|
54
|
+
&& url.hostname.endsWith(".cloudflareaccess.com")
|
|
55
|
+
? url.origin
|
|
56
|
+
: null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function accessIdentity(request: Request, env: Env, ctx: AccessContext): Promise<AccessIdentity | null> {
|
|
63
|
+
if (ctx.access) {
|
|
64
|
+
try {
|
|
65
|
+
return await ctx.access.getIdentity() as AccessIdentity;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("Cloudflare Access runtime identity was unavailable", error instanceof Error ? error.message : typeof error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const token = request.headers.get("cf-access-jwt-assertion");
|
|
71
|
+
const audience = bounded(env.ACCESS_AUD, 200);
|
|
72
|
+
if (!token || !audience) return null;
|
|
73
|
+
try {
|
|
74
|
+
const tokenIssuer = issuer(decodeJwt(token).iss);
|
|
75
|
+
if (!tokenIssuer) return null;
|
|
76
|
+
let keySet = accessKeySets.get(tokenIssuer);
|
|
77
|
+
if (!keySet) {
|
|
78
|
+
keySet = createRemoteJWKSet(new URL("/cdn-cgi/access/certs", tokenIssuer));
|
|
79
|
+
accessKeySets.set(tokenIssuer, keySet);
|
|
80
|
+
}
|
|
81
|
+
const { payload } = await jwtVerify(token, keySet, {
|
|
82
|
+
algorithms: ["RS256"], audience, issuer: tokenIssuer,
|
|
83
|
+
});
|
|
84
|
+
return payload as AccessIdentity;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn("Cloudflare Access JWT validation failed", error instanceof Error ? error.message : typeof error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function resolveOperatorSession(request: Request, env: Env): Promise<OperatorSession | null> {
|
|
92
|
+
return (await resolveOperatorAuthentication(request, env))?.session ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ResolvedOperatorAuthentication {
|
|
96
|
+
session: OperatorSession;
|
|
97
|
+
csrfHash?: string;
|
|
98
|
+
csrfToken?: string;
|
|
99
|
+
expiresAt?: Date;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function resolveOperatorAuthentication(
|
|
103
|
+
request: Request,
|
|
104
|
+
env: Env,
|
|
105
|
+
): Promise<ResolvedOperatorAuthentication | null> {
|
|
106
|
+
const ownerSession = await resolveOperatorAccountSession(request, env.DB);
|
|
107
|
+
if (ownerSession) return { session: ownerSession.session, csrfHash: ownerSession.csrfHash };
|
|
108
|
+
const better = await resolveBetterAuthSession(request, env);
|
|
109
|
+
if (better) {
|
|
110
|
+
const session = await resolveBetterAuthOperator(env.DB, {
|
|
111
|
+
email: better.user.email,
|
|
112
|
+
name: better.user.name,
|
|
113
|
+
image: better.user.image,
|
|
114
|
+
}, await invitationIdFromRequest(request, env));
|
|
115
|
+
if (session) {
|
|
116
|
+
const csrfToken = await betterAuthCsrfToken(env, better.session.id);
|
|
117
|
+
return {
|
|
118
|
+
session,
|
|
119
|
+
csrfHash: await betterAuthCsrfHash(csrfToken),
|
|
120
|
+
csrfToken,
|
|
121
|
+
expiresAt: better.session.expiresAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const adminKey = request.headers.get("x-admin-key");
|
|
126
|
+
if (adminKey && env.ADMIN_API_KEY && await secretsEqual(adminKey, env.ADMIN_API_KEY)) {
|
|
127
|
+
return { session: {
|
|
128
|
+
actor: { type: "admin_key", id: "deployment-owner" },
|
|
129
|
+
authMethod: "admin_key",
|
|
130
|
+
verified: false,
|
|
131
|
+
displayName: "Deployment owner",
|
|
132
|
+
roles: ["owner"],
|
|
133
|
+
permissions: permissionsForRoles(["owner"]),
|
|
134
|
+
queueScope: null,
|
|
135
|
+
fieldScope: null,
|
|
136
|
+
} };
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function appendOperatorAuthenticationCookies(
|
|
142
|
+
response: Response,
|
|
143
|
+
request: Request,
|
|
144
|
+
authentication: ResolvedOperatorAuthentication,
|
|
145
|
+
): Response {
|
|
146
|
+
return authentication.csrfToken && authentication.expiresAt
|
|
147
|
+
? appendBetterAuthCsrfCookie(response, request, authentication.csrfToken, authentication.expiresAt)
|
|
148
|
+
: response;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function resolveInfrastructureOwnerIdentity(
|
|
152
|
+
request: Request,
|
|
153
|
+
env: Env,
|
|
154
|
+
ctx: AccessContext,
|
|
155
|
+
): Promise<{ email: string; displayName: string; avatarUrl?: string } | null> {
|
|
156
|
+
const identity = await accessIdentity(request, env, ctx);
|
|
157
|
+
if (!identity) return null;
|
|
158
|
+
const email = bounded(identity.email, 254)?.toLowerCase();
|
|
159
|
+
if (!email) return null;
|
|
160
|
+
const allowedOwners = new Set((env.INFRASTRUCTURE_OWNER_EMAILS ?? "").split(",")
|
|
161
|
+
.map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
162
|
+
if (!allowedOwners.has(email)) return null;
|
|
163
|
+
const picture = avatarUrl(identity.picture);
|
|
164
|
+
return {
|
|
165
|
+
email,
|
|
166
|
+
displayName: bounded(identity.name, 100) ?? email,
|
|
167
|
+
...(picture ? { avatarUrl: picture } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function requireOperator(
|
|
172
|
+
request: Request,
|
|
173
|
+
env: Env,
|
|
174
|
+
permission: keyof OperatorPermissions,
|
|
175
|
+
): Promise<OperatorSession> {
|
|
176
|
+
const authentication = await resolveOperatorAuthentication(request, env);
|
|
177
|
+
const session = authentication?.session;
|
|
178
|
+
if (!session) throw new ApiError(401, "operator_authentication_required", "Sign in to your Safest workspace to continue.");
|
|
179
|
+
if (authentication.csrfHash) await verifyOperatorCsrf(request, authentication.csrfHash);
|
|
180
|
+
if (!session.permissions[permission]) throw new ApiError(403, "operator_permission_denied", "Your role does not permit this operation.");
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function assertOperatorQueueAccess(session: OperatorSession, queueId: string | null): void {
|
|
185
|
+
if (session.queueScope === null || session.queueScope === undefined) return;
|
|
186
|
+
if (!queueId || !session.queueScope.includes(queueId)) throw new ApiError(403, "operator_queue_scope_denied", "Your role is not authorized for this queue.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function assertOperatorReportAccess(
|
|
190
|
+
db: D1Database,
|
|
191
|
+
session: OperatorSession,
|
|
192
|
+
reportId: string,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
if (session.queueScope === null || session.queueScope === undefined) return;
|
|
195
|
+
const report = await db.prepare(`SELECT queue_id AS queueId FROM reports WHERE id = ?1 LIMIT 1`)
|
|
196
|
+
.bind(reportId).first<{ queueId: string | null }>();
|
|
197
|
+
if (!report) throw new ApiError(404, "report_not_found", "The report does not exist.");
|
|
198
|
+
assertOperatorQueueAccess(session, report.queueId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function assertOperatorWorkflowRunAccess(
|
|
202
|
+
db: D1Database,
|
|
203
|
+
session: OperatorSession,
|
|
204
|
+
runId: string,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (session.queueScope === null || session.queueScope === undefined) return;
|
|
207
|
+
const run = await db.prepare(`SELECT queue_id AS queueId FROM workflow_runs WHERE id = ?1 LIMIT 1`)
|
|
208
|
+
.bind(runId).first<{ queueId: string | null }>();
|
|
209
|
+
if (!run) throw new ApiError(404, "workflow_run_not_found", "The workflow run does not exist.");
|
|
210
|
+
assertOperatorQueueAccess(session, run.queueId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function projectOperatorFields<T>(session: OperatorSession, resource: string, value: T): T {
|
|
214
|
+
if (session.fieldScope === null || session.fieldScope === undefined) return value;
|
|
215
|
+
const allowed = new Set(session.fieldScope[resource] ?? []);
|
|
216
|
+
const project = (entry: unknown): unknown => {
|
|
217
|
+
if (Array.isArray(entry)) return entry.map(project);
|
|
218
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
219
|
+
return Object.fromEntries(Object.entries(entry as Record<string, unknown>).filter(([key]) => allowed.has(key)));
|
|
220
|
+
};
|
|
221
|
+
return project(value) as T;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bearer(request: Request): string | null {
|
|
225
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
226
|
+
const match = /^Bearer\s+([^\s]+)$/iu.exec(authorization);
|
|
227
|
+
return match?.[1] ?? null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function requireIntegration(request: Request, env: Env): Promise<{
|
|
231
|
+
id: string;
|
|
232
|
+
allowedOrigins: string[];
|
|
233
|
+
scopes: string[];
|
|
234
|
+
channelMode: "contextual" | "anonymous";
|
|
235
|
+
formVersionId: string | null;
|
|
236
|
+
contextSchema: unknown;
|
|
237
|
+
}> {
|
|
238
|
+
const integrationId = request.headers.get("x-safest-integration-id")?.trim();
|
|
239
|
+
const suppliedKey = bearer(request);
|
|
240
|
+
if (!integrationId || integrationId.length > 200 || !suppliedKey || suppliedKey.length > 500) {
|
|
241
|
+
throw new ApiError(401, "integration_authentication_required", "A valid integration ID and bearer credential are required.");
|
|
242
|
+
}
|
|
243
|
+
let configured: unknown = {};
|
|
244
|
+
let environmentConfigurationValid = true;
|
|
245
|
+
try {
|
|
246
|
+
configured = JSON.parse(env.INTEGRATION_API_KEYS_JSON ?? "{}");
|
|
247
|
+
} catch {
|
|
248
|
+
environmentConfigurationValid = false;
|
|
249
|
+
}
|
|
250
|
+
if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
|
|
251
|
+
configured = {};
|
|
252
|
+
environmentConfigurationValid = false;
|
|
253
|
+
}
|
|
254
|
+
const integration = await loadIntegration(env.DB, integrationId);
|
|
255
|
+
const expectedKey = integration ? (configured as Record<string, unknown>)[integration.key_id] : undefined;
|
|
256
|
+
const managedKey = integration ? await env.DB.prepare(`
|
|
257
|
+
SELECT key_hash AS keyHash FROM integration_api_keys
|
|
258
|
+
WHERE integration_id = ?1 AND status = 'active' AND valid_from <= ?2
|
|
259
|
+
AND (expires_at IS NULL OR expires_at > ?2)
|
|
260
|
+
ORDER BY created_at DESC, id DESC
|
|
261
|
+
`).bind(integration.id, new Date().toISOString()).all<{ keyHash: string }>() : { results: [] as { keyHash: string }[] };
|
|
262
|
+
const suppliedHash = await sha256(`integration-key-v1:${suppliedKey}`);
|
|
263
|
+
const managedValid = (await Promise.all(managedKey.results.map((entry) => secretsEqual(suppliedHash, entry.keyHash)))).some(Boolean);
|
|
264
|
+
const environmentValid = typeof expectedKey === "string" && expectedKey.length >= 16 && await secretsEqual(suppliedKey, expectedKey);
|
|
265
|
+
const valid = managedValid || environmentValid;
|
|
266
|
+
if (!integration || integration.enabled !== 1 || !valid) {
|
|
267
|
+
if (integration && !environmentConfigurationValid && managedKey.results.length === 0) {
|
|
268
|
+
throw new ApiError(503, "integration_credentials_misconfigured", "Integration credentials are not configured correctly.");
|
|
269
|
+
}
|
|
270
|
+
throw new ApiError(401, "invalid_integration_credential", "The integration credential is invalid.");
|
|
271
|
+
}
|
|
272
|
+
let allowedOrigins: string[] = [];
|
|
273
|
+
try {
|
|
274
|
+
const parsed: unknown = JSON.parse(integration.allowed_origins_json);
|
|
275
|
+
if (Array.isArray(parsed)) allowedOrigins = parsed.filter((value): value is string => typeof value === "string");
|
|
276
|
+
} catch {
|
|
277
|
+
allowedOrigins = [];
|
|
278
|
+
}
|
|
279
|
+
const scopesRow = await env.DB.prepare(`SELECT scopes_json AS scopesJson FROM application_integrations WHERE id = ?1`).bind(integration.id).first<{ scopesJson: string }>();
|
|
280
|
+
let scopes: string[] = [];
|
|
281
|
+
try { const parsed = JSON.parse(scopesRow?.scopesJson ?? "[]"); if (Array.isArray(parsed)) scopes = parsed.filter((entry): entry is string => typeof entry === "string"); } catch { scopes = []; }
|
|
282
|
+
let contextSchema: unknown = undefined;
|
|
283
|
+
try { contextSchema = JSON.parse(integration.context_schema_json); } catch { contextSchema = undefined; }
|
|
284
|
+
return {
|
|
285
|
+
id: integration.id,
|
|
286
|
+
allowedOrigins,
|
|
287
|
+
scopes,
|
|
288
|
+
channelMode: integration.channel_mode,
|
|
289
|
+
formVersionId: integration.form_version_id,
|
|
290
|
+
contextSchema,
|
|
291
|
+
};
|
|
292
|
+
}
|