Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,6 @@ INGEST_REQUIRE_TLS=false
# space and angle brackets, which an unquoted dotenv value can't hold.
# EMAIL_FROM="Maple <notifications@noreply.maple.dev>"

# Hazel integration (OAuth)
# Maple acts as an OIDC client against Hazel's Clerk-hosted OAuth provider.
# Register Maple as an OAuth Application in Hazel's Clerk dashboard, then drop
# the credentials here. Defaults target https://clerk.hazel.sh + https://api.hazel.sh.
# HAZEL_API_BASE_URL=https://api.hazel.sh
# HAZEL_OAUTH_DISCOVERY_URL=https://clerk.hazel.sh/.well-known/openid-configuration
# HAZEL_OAUTH_CLIENT_ID=
# HAZEL_OAUTH_CLIENT_SECRET=
# HAZEL_OAUTH_SCOPES=openid email profile organizations:read channels:read channel-webhooks:write

# GitHub integration (GitHub App)
# Maple connects to GitHub via a GitHub App for repo access, OAuth sign-in, and
# webhooks. Create one at https://github.com/settings/apps (or your org's
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,4 @@ Vitest uses embedded PGlite — no Docker Postgres required for unit tests.
- [docs/persistence.md](docs/persistence.md) — Postgres migrations
- [docs/self-hosted-clickhouse.md](docs/self-hosted-clickhouse.md) — ClickHouse BYO + schema
- [docs/local-mode.md](docs/local-mode.md) — single-binary local mode
- [.env.example](.env.example) — full env reference with optional integrations (GitHub App, Hazel, Autumn, email)
- [.env.example](.env.example): full env reference with optional integrations (GitHub App, Autumn, email)
5 changes: 0 additions & 5 deletions apps/api/src/resources/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ export const apiConfiguredEnv = (stage: MapleStage, region: MapleRegion, domains
optionalPlain("MAPLE_CLERK_DASHBOARD_URL"),
optionalSecret("SD_INTERNAL_TOKEN"),
optionalSecret("INTERNAL_SERVICE_TOKEN"),
optionalPlain("HAZEL_API_BASE_URL"),
optionalPlain("HAZEL_OAUTH_DISCOVERY_URL"),
optionalPlain("HAZEL_OAUTH_CLIENT_ID"),
optionalSecret("HAZEL_OAUTH_CLIENT_SECRET"),
optionalPlain("HAZEL_OAUTH_SCOPES"),
optionalPlain("LINEAR_OAUTH_CLIENT_ID"),
optionalSecret("LINEAR_OAUTH_CLIENT_SECRET"),
// Each connector's declared install and outbound config; unset means unavailable.
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/routes/integrations-callback-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const render = (returnTo: string | null, targetOrigin = "https://web.maple.dev")
status: "success",
message: "Connected",
returnTo,
messageType: "maple:integration:hazel",
label: "Hazel",
messageType: "maple:integration:linear",
label: "Linear",
targetOrigin,
})

Expand Down
11 changes: 0 additions & 11 deletions apps/api/src/routes/integrations-callback.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ import { Env } from "@maple/backend/platform/Env"
import { PLANETSCALE_CALLBACK_PATH } from "@maple/backend/services/auth/PlanetScaleOAuthService"
import { SUPABASE_CALLBACK_PATH } from "@maple/backend/services/auth/SupabaseOAuthService"

export const HAZEL_CALLBACK_PATH = "/api/integrations/hazel/callback"
export const LINEAR_CALLBACK_PATH = "/api/integrations/linear/callback"
export const GITHUB_CALLBACK_PATH = "/api/integrations/github/callback"
export const CLOUDFLARE_CALLBACK_PATH = "/api/integrations/cloudflare/callback"

const HAZEL_MESSAGE_TYPE = "maple:integration:hazel"
const LINEAR_MESSAGE_TYPE = "maple:integration:linear"
const GITHUB_MESSAGE_TYPE = "maple:integration:github"
const CLOUDFLARE_MESSAGE_TYPE = "maple:integration:cloudflare"
Expand Down Expand Up @@ -243,15 +241,6 @@ const requireInstallation = (params: URLSearchParams): string | null =>
: null

const ROUTES: ReadonlyArray<CallbackRoute> = [
{
path: HAZEL_CALLBACK_PATH,
flow: "hazel",
provider: "hazel",
label: "Hazel",
page: "/integrations",
messageType: HAZEL_MESSAGE_TYPE,
precheck: requireCodeAndState,
},
{
path: LINEAR_CALLBACK_PATH,
flow: "linear",
Expand Down
85 changes: 1 addition & 84 deletions apps/api/src/routes/internal/integrations.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
CloudflareTopTrafficRow,
CloudflareUsageResponse,
CurrentTenant,
ExternalUserId,
GcpResourcesResponse,
GithubDeleteRepositoryResponse,
GithubDisconnectResponse,
Expand All @@ -21,11 +20,6 @@ import {
GithubPrReviewSettingsResponse,
GithubSetTrackedBranchResponse,
GithubStartConnectResponse,
HazelChannelsListResponse,
HazelDisconnectResponse,
HazelIntegrationStatus,
HazelOrganizationsListResponse,
HazelStartConnectResponse,
IntegrationsForbiddenError,
IntegrationsUpstreamError,
LinearDisconnectResponse,
Expand All @@ -38,7 +32,6 @@ import {
RoleName,
SupabaseConnectResponse,
SupabaseDisconnectResponse,
UserId,
VCS_COMMIT_DETAILS_MAX_SHAS,
VCS_COMMIT_RANGES_MAX,
VcsCommitDetailResponse,
Expand All @@ -49,7 +42,7 @@ import {
VcsPullRequestsResponse,
} from "@maple/domain/http"
import { EdgeCacheService } from "@maple/cache"
import { Effect, Option, Schema } from "effect"
import { Effect, Option } from "effect"
import { Env } from "@maple/backend/platform/Env"
import { graphqlQuery } from "@maple/backend/services/integrations/CloudflareApi"
import { CloudflareAnalyticsService } from "@maple/backend/services/integrations/CloudflareAnalyticsService"
Expand All @@ -74,21 +67,16 @@ import {
import { GithubConnectService } from "@maple/backend/services/integrations/vcs/vendor/github/GithubConnectService"
import { VcsCommitService } from "@maple/backend/services/integrations/vcs/VcsCommitService"
import { VcsSourceService } from "@maple/backend/services/integrations/vcs/VcsSourceService"
import { HazelOAuthService } from "@maple/backend/services/auth/HazelOAuthService"
import { LinearOAuthService } from "@maple/backend/services/auth/LinearOAuthService"
import { requireAdmin as requireAdminRole } from "@maple/backend/services/auth/auth"
import {
CLOUDFLARE_CALLBACK_PATH,
GITHUB_CALLBACK_PATH,
HAZEL_CALLBACK_PATH,
LINEAR_CALLBACK_PATH,
} from "@/routes/integrations-callback.http"
import { isTrustedCallbackOrigin, resolveRequestOrigin } from "@/routes/v2/integrations.http"
import { makeOAuthCompleter } from "@/routes/oauth-complete"

const asExternalUserId = Schema.decodeUnknownSync(ExternalUserId)
const asUserId = Schema.decodeUnknownSync(UserId)

/**
* How long `cloudflarePrime` spends on the post-connect poll. Long enough for zone discovery plus
* a first window on an ordinary account; whatever a slow or many-zoned one does not finish resumes
Expand All @@ -105,7 +93,6 @@ const requireAdmin = (roles: ReadonlyArray<RoleName>) =>

export const HttpIntegrationsLive = HttpApiBuilder.group(MapleInternalApi, "integrations", (handlers) =>
Effect.gen(function* () {
const hazel = yield* HazelOAuthService
const linear = yield* LinearOAuthService
const github = yield* GithubConnectService
const vcsCommits = yield* VcsCommitService
Expand Down Expand Up @@ -144,76 +131,6 @@ export const HttpIntegrationsLive = HttpApiBuilder.group(MapleInternalApi, "inte
return yield* completeOAuth(payload, { orgId: tenant.orgId, userId: tenant.userId })
}),
)
.handle("hazelStatus", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
const status = yield* hazel.getStatus(tenant.orgId)
if (!status.connected) {
return new HazelIntegrationStatus({
connected: false,
externalUserId: null,
externalUserEmail: null,
connectedByUserId: null,
scope: null,
})
}
return new HazelIntegrationStatus({
connected: true,
externalUserId: asExternalUserId(status.externalUserId),
externalUserEmail: status.externalUserEmail,
connectedByUserId: asUserId(status.connectedByUserId),
scope: status.scope,
})
}),
)
.handle("hazelStart", ({ payload }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles)
const callbackUrl = yield* trustedCallbackUrl(HAZEL_CALLBACK_PATH)
const result = yield* hazel.startConnect(tenant.orgId, tenant.userId, {
callbackUrl,
returnTo: payload.returnTo,
})
return new HazelStartConnectResponse(result)
}),
)
.handle("hazelOrganizations", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
const organizations = yield* hazel.listOrganizations(tenant.orgId)
return new HazelOrganizationsListResponse({
organizations: organizations.map((o) => ({
id: o.id,
name: o.name,
slug: o.slug,
logoUrl: o.logoUrl,
})),
})
}),
)
.handle("hazelChannels", ({ params }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
const channels = yield* hazel.listChannels(tenant.orgId, params.organizationId)
return new HazelChannelsListResponse({
channels: channels.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
organizationId: c.organizationId,
})),
})
}),
)
.handle("hazelDisconnect", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles)
const result = yield* hazel.disconnect(tenant.orgId)
return new HazelDisconnectResponse(result)
}),
)
.handle("linearStatus", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
Expand Down
7 changes: 0 additions & 7 deletions apps/api/src/routes/oauth-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Effect } from "effect"
import { summarizeCause } from "@maple/backend/platform/describe-cause"
import type { OAuthFlowInitiator } from "@maple/backend/services/auth/oauth/oauth-state"
import { CloudflareOAuthService } from "@maple/backend/services/auth/CloudflareOAuthService"
import { HazelOAuthService } from "@maple/backend/services/auth/HazelOAuthService"
import { LinearOAuthService } from "@maple/backend/services/auth/LinearOAuthService"
import { PlanetScaleOAuthService } from "@maple/backend/services/auth/PlanetScaleOAuthService"
import { SupabaseOAuthService } from "@maple/backend/services/auth/SupabaseOAuthService"
Expand All @@ -20,7 +19,6 @@ import { GithubConnectService } from "@maple/backend/services/integrations/vcs/v

const LABELS = {
cloudflare: "Cloudflare",
hazel: "Hazel",
linear: "Linear",
github: "GitHub",
planetscale: "PlanetScale",
Expand Down Expand Up @@ -77,7 +75,6 @@ const failure = (message: string) => new OAuthCompleteResponse({ status: "error"
* `initiator`, so a callback query carried into another person's session completes nothing.
*/
export const makeOAuthCompleter = Effect.gen(function* () {
const hazel = yield* HazelOAuthService
const linear = yield* LinearOAuthService
const github = yield* GithubConnectService
const cloudflare = yield* CloudflareOAuthService
Expand Down Expand Up @@ -239,10 +236,6 @@ export const makeOAuthCompleter = Effect.gen(function* () {
switch (request.flow) {
case "cloudflare":
return withCode(params, (code, state) => completeCloudflare(code, state, initiator))
case "hazel":
return withCode(params, (code, state) =>
hazel.completeConnect(code, state, initiator).pipe(Effect.as(success(CLOSE_HINT))),
)
case "linear":
return withCode(params, (code, state) =>
linear.completeConnect(code, state, initiator).pipe(Effect.as(success(CLOSE_HINT))),
Expand Down
4 changes: 0 additions & 4 deletions apps/api/src/routes/v2/alchemy-provider.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import { AlertReadModelsService } from "@maple/backend/services/alerts/AlertRead
import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesService"
import { LinearOAuthService } from "@maple/backend/services/auth/LinearOAuthService"
import { LinearIssueTracker } from "@maple/backend/services/alerts/LinearIssueTracker"
import { HazelOAuthService } from "@maple/backend/services/auth/HazelOAuthService"
import { OrgClickHouseSettingsService } from "@maple/backend/services/org/OrgClickHouseSettingsService"
import { OrgMembersService } from "@maple/backend/services/org/OrgMembersService"
import { QueryEngineService } from "@maple/backend/services/warehouse/QueryEngineService"
Expand Down Expand Up @@ -121,7 +120,6 @@ const makeHarness = () => {
schedulerEvalTimeoutMs: () => 15_000,
schedulerSpreadMs: () => 0,
})
const hazelOAuthLive = HazelOAuthService.layer.pipe(Layer.provide(Layer.mergeAll(envLive, testDb.layer)))
const linearOAuthLive = LinearOAuthService.layer.pipe(
Layer.provide(Layer.mergeAll(envLive, testDb.layer)),
)
Expand Down Expand Up @@ -152,7 +150,6 @@ const makeHarness = () => {
envLive,
testDb.layer,
runtimeLive,
hazelOAuthLive,
linearOAuthLive,
linearIssuesLive,
emailLive,
Expand Down Expand Up @@ -183,7 +180,6 @@ const makeHarness = () => {
queryEngineLive,
warehouseLive,
runtimeLive,
hazelOAuthLive,
linearOAuthLive,
linearIssuesLive,
emailLive,
Expand Down
57 changes: 1 addition & 56 deletions apps/api/src/routes/v2/alert-destinations.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
CurrentTenant,
DiscordAlertDestinationConfig,
EmailAlertDestinationConfig,
HazelOAuthAlertDestinationConfig,
LinearAlertDestinationConfig,
AlertDestinationNotFoundError,
PagerDutyAlertDestinationConfig,
Expand Down Expand Up @@ -68,21 +67,6 @@ const toCreateRequest = (params: V2AlertDestinationCreateParams) => {
: undefined),
...(params.enabled !== undefined ? { enabled: params.enabled } : undefined),
})
case "hazel-oauth":
return new HazelOAuthAlertDestinationConfig({
type: "hazel-oauth",
name: params.name,
hazelOrganizationId: params.hazel_organization_id,
hazelOrganizationName: params.hazel_organization_name,
...(params.hazel_organization_logo_url !== undefined
? {
hazelOrganizationLogoUrl: params.hazel_organization_logo_url,
}
: undefined),
hazelChannelId: params.hazel_channel_id,
hazelChannelName: params.hazel_channel_name,
...(params.enabled !== undefined ? { enabled: params.enabled } : undefined),
})
case "discord":
return new DiscordAlertDestinationConfig({
type: "discord",
Expand Down Expand Up @@ -153,34 +137,6 @@ const toUpdateRequest = (params: V2AlertDestinationUpdateParams): AlertDestinati
? { signingSecret: params.signing_secret }
: undefined),
}
case "hazel-oauth":
return {
type: "hazel-oauth",
...shared,
...(params.hazel_organization_id !== undefined
? {
hazelOrganizationId: params.hazel_organization_id,
}
: undefined),
...(params.hazel_organization_name !== undefined
? {
hazelOrganizationName: params.hazel_organization_name,
}
: undefined),
...(params.hazel_organization_logo_url !== undefined
? {
hazelOrganizationLogoUrl: params.hazel_organization_logo_url,
}
: undefined),
...(params.hazel_channel_id !== undefined
? { hazelChannelId: params.hazel_channel_id }
: undefined),
...(params.hazel_channel_name !== undefined
? {
hazelChannelName: params.hazel_channel_name,
}
: undefined),
}
case "discord":
return {
type: "discord",
Expand Down Expand Up @@ -242,18 +198,7 @@ export const destinationAuditDiff = auditDiff({
writeOnly: ["integration_key", "signing_secret", "url", "webhook_url", "bot_token"],
// Provider-side handles. Not secret, but not readable back off the document
// either — recorded as touched so the change is not invisible.
opaque: [
"channel_id",
"channel_name",
"chat_id",
"workspace_id",
"hazel_organization_id",
"hazel_organization_name",
"hazel_organization_logo_url",
"hazel_channel_id",
"hazel_channel_name",
"team_id",
],
opaque: ["channel_id", "channel_name", "chat_id", "workspace_id", "team_id"],
})

export const HttpV2AlertDestinationsLive = HttpApiBuilder.group(MapleApiV2, "alertDestinations", (handlers) =>
Expand Down
Loading
Loading