Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
286bbb0
docs: add full Foldkit/Effect audit report
Makisuo Oct 15, 2026
80ef59a
chore: remove the UI parity harness and the dev gallery
Makisuo Oct 15, 2026
c141b38
refactor(web): delete gallery-only kit components and dead exports
Makisuo Oct 15, 2026
3485ffe
refactor(web): typed error reasons and Effect optimistic actions, dro…
Makisuo Oct 15, 2026
39c4698
refactor(auth): remove the Clerk sign-in path
Makisuo Oct 15, 2026
36f2ece
fix(backend): keep failure causes server-side and stop policy DB erro…
Makisuo Oct 15, 2026
5346e2a
refactor(web): render messages with the editor's parser and decoratio…
Makisuo Oct 15, 2026
44c9a66
fix(backend): log server failures centrally and stop sending driver t…
Makisuo Oct 15, 2026
3a7aec2
refactor(web): drop React Aria DOM emulation for native Tailwind vari…
Makisuo Oct 15, 2026
5759d60
refactor(web): typed errors at the core's Promise boundaries, drop de…
Makisuo Oct 15, 2026
7af15cf
docs(web): finish the error convention; storage reads catch their own…
Makisuo Oct 15, 2026
e6168f9
refactor(web): Slider and Tabs on @foldkit/ui, with the value owned b…
Makisuo Oct 15, 2026
3c8ef37
fix(backend): let outbox side effects fail so the outbox retries
Makisuo Oct 15, 2026
f53581f
fix(backend): answer bad input with 400s and missing records with not…
Makisuo Oct 15, 2026
a9e21a5
fix(auth): fail sign-in when verified-email linking fails instead of …
Makisuo Oct 15, 2026
52c8580
refactor(web): Menu entries come from the parent through view inputs
Makisuo Oct 15, 2026
4d10568
refactor(web): Select items come from the parent through view inputs
Makisuo Oct 15, 2026
b64ea79
refactor(web): ComboBox items come from the parent
Makisuo Oct 15, 2026
72eebee
fix(web): Create a new one on the org picker opens the workspace form…
Makisuo Oct 15, 2026
c546e9c
refactor(web): typed auth failure reasons and submission unions on th…
Makisuo Oct 15, 2026
faa2a1d
refactor(web): date segments keep their locale in the Model with cach…
Makisuo Oct 15, 2026
e5116e7
chore(web): drop legacy-sharing comments and delegate to Interaction.…
Makisuo Oct 15, 2026
3e4b1bd
refactor(backend): typed failures instead of defects, catch-alls and …
Makisuo Oct 15, 2026
ef4b982
docs: point CLAUDE.md at apps/backend and Hazel auth
Makisuo Oct 15, 2026
3c883b1
refactor(web): appearance previews are static SVG files
Makisuo Oct 15, 2026
a35625e
refactor(web): typed failures and retryable lookups on join, invite a…
Makisuo Oct 15, 2026
6f7a53e
Merge branch 'worktree-agent-aea3afaba4a37dc74' into chore/foldkit-au…
Makisuo Oct 15, 2026
2431a54
fix(web): drop the removed ClerkUserFetchError from the common error …
Makisuo Oct 15, 2026
e70a4e0
refactor(web): explicit loading unions for the entry pages' live queries
Makisuo Oct 15, 2026
d42b977
fix(web): icons are decorative by default and icon-only controls are …
Makisuo Oct 15, 2026
b5cc0d8
docs: record the end of React Aria DOM parity in the S5 decision
Makisuo Oct 15, 2026
0ea4953
fix(web): message list skips ApplyScroll when its element is gone; Sc…
Makisuo Oct 15, 2026
56f9493
refactor(web): unfurls as AsyncData with a timeout; keyed reaction pi…
Makisuo Oct 15, 2026
4d7eb1c
refactor(web): reshape the channel page and fix the chat P0s
Makisuo Oct 15, 2026
82100a8
Merge branch 'worktree-agent-a88c4576e64bf74f6' into chore/foldkit-au…
Makisuo Oct 15, 2026
018dcba
fix(web): emoji and GIF pickers hold AsyncData with typed failures an…
Makisuo Oct 15, 2026
744101a
wip(web): root/shell/platform audit fixes (in progress)
Makisuo Oct 15, 2026
7dcf558
fix(web): modal results route by instance; delete-message failure toasts
Makisuo Oct 15, 2026
a5593f3
fix(web): composer draft owned by the Model, interruptible uploads, E…
Makisuo Oct 15, 2026
d38fba8
Merge chore/foldkit-audit-fixes into root stream
Makisuo Oct 15, 2026
56811a1
Merge chore/foldkit-audit-fixes into composer-overlay stream
Makisuo Oct 15, 2026
0dab2b8
Merge chore/foldkit-audit-fixes (kit stream) into the chat stream
Makisuo Oct 15, 2026
f4d3e44
refactor(web): onboarding as one Phase/StepState union with retryable…
Makisuo Oct 15, 2026
a3926dc
test(web): shell, sidebar and presence stories for the reshaped models
Makisuo Oct 15, 2026
adc90d5
Merge chore/foldkit-audit-fixes (composer/overlay stream) into the ch…
Makisuo Oct 15, 2026
0256bbc
Merge chore/foldkit-audit-fixes (modal context, editor registry) into…
Makisuo Oct 15, 2026
d229a83
Merge chore/foldkit-audit-fixes into the auth/pages stream
Makisuo Oct 15, 2026
59e2ca4
refactor(web): chat reaction and pin actions report typed reasons (s6)
Makisuo Oct 15, 2026
5eb8519
Merge branch 'worktree-agent-a9df5c7619c33b160' into chore/foldkit-au…
Makisuo Oct 15, 2026
71dbdfe
refactor(web): shared ConfirmAction and DropZone Submodels, typed upl…
Makisuo Oct 15, 2026
dcb99a2
fix(web): settings pages show load failures, typed failure reasons, A…
Makisuo Oct 15, 2026
046d148
fix(web): my-settings form validation copy, typed upload/autostart fa…
Makisuo Oct 15, 2026
e7fd252
fix(web): channel settings Save rebases after saving, share search sh…
Makisuo Oct 15, 2026
feb6126
fix(web): typed session/org states, bounded retries, route-based nav;…
Makisuo Oct 15, 2026
6863818
Merge chore/foldkit-audit-fixes into the settings stream
Makisuo Oct 15, 2026
ad6a482
Merge chore/foldkit-audit-fixes (auth pages, chat) into root stream; …
Makisuo Oct 15, 2026
d2f3f01
Merge chore/foldkit-audit-fixes (settings) into root stream; settings…
Makisuo Oct 15, 2026
9ff114d
refactor(web): toasts on @foldkit/ui Toast; drop the sonner port
Makisuo Oct 15, 2026
34bb57d
refactor(web): tooltips on @foldkit/ui Tooltip; delete the global mod…
Makisuo Oct 15, 2026
94319da
refactor(web): Modal and Sheet on @foldkit/ui Dialog (native <dialog>)
Makisuo Oct 15, 2026
35a049a
Merge chore/foldkit-audit-fixes into overlay-primitives stream
Makisuo Oct 15, 2026
7657bbb
refactor(web): Popover on @foldkit/ui Popover with Anchor positioning
Makisuo Oct 15, 2026
c1cf0c3
refactor(web): modal open and close are Model changes
Makisuo Oct 15, 2026
ec72f4e
refactor(web): stateless overlay helpers on Foldkit Anchor and Dom
Makisuo Oct 15, 2026
d620f82
refactor(web): pending-button announcer as an app Resource; no ui/ari…
Makisuo Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 0 additions & 8 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,6 @@
"foldkit/wrap-child-output-in-got-message": "off"
}
},
{
// ui/aria ports React Aria's document-wide state (global input modality, scroll locks, the live
// announcer) as module state on purpose; see docs/foldkit-decisions/s5-primitives.md.
"files": ["apps/web-foldkit/src/ui/aria/**"],
"rules": {
"foldkit/no-module-level-mutable-state": "off"
}
},
{
// Framework-free modules carried over from the React app: imperative singletons and
// helpers, not Foldkit programs, so the Foldkit architecture rules do not apply.
Expand Down
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is a monorepo with the following structure:

- `apps/web-foldkit/` - Web frontend built with Foldkit (Effect-based Elm architecture), Vite and TailwindCSS. Also the desktop app's webview
- `apps/backendv2/` - Backend API using Bun runtime and Effect-TS
- `apps/backend/` - Backend API and RPC (Effect-TS); runs on Bun locally and as a Cloudflare Worker
- `apps/cluster/` - Effect Cluster service for distributed workflows and background jobs
- `packages/db/` - Shared database package: effect-orm table definitions, the `Database` service, and migrations
- `packages/domain/` - Shared domain types, RPC contracts, and cluster definitions
Expand Down Expand Up @@ -80,10 +80,12 @@ When working with Effect, Effect Atom, or TanStack DB, refer to these directorie
- `bun run typecheck` - TypeScript checking without emitting files
- `bun run test` - Run the app's Vitest suite

### Backend (apps/backendv2)
### Backend (apps/backend)

- `bun run dev` - Start backend with hot reload using Bun (DO NOT USE - already running)
- `bun run typecheck` - TypeScript checking
- `bun run typecheck` - TypeScript checking (Bun entry and the Worker's `tsconfig.worker.json`)
- `bun run test` - Run the backend's Vitest suite (Postgres-backed tests start a Docker container; set `AUTH_TEST_DATABASE_URL` to use an existing database)
- `bun run test:sync` / `bun run test:policies` - Chat-sync and outbox, or policy, tests only

### Cluster (apps/cluster)

Expand Down Expand Up @@ -115,7 +117,7 @@ When working with Effect, Effect Atom, or TanStack DB, refer to these directorie
- **Runtime**: Bun
- **Framework**: Effect-TS for functional programming
- **Database**: PostgreSQL with effect-orm (`@maple-dev/effect-orm`) over `@effect/sql-pg`
- **Auth**: Clerk (`@clerk/backend` JWT verification + webhook sync)
- **Auth**: Hazel auth (`@hazel/auth`, on `@yielded/auth`): GitHub/Google sign-in, passkeys, cookie sessions; Clerk is only used to verify Maple's OAuth tokens (`services/oauth-bearer-auth.ts`)
- **API**: RPC-style endpoints via Effect Http Api

### Cluster Service
Expand Down Expand Up @@ -212,7 +214,7 @@ yield *

- Backend follows Effect-TS patterns for error handling and dependency injection
- Real-time features implemented via Cloudflare Realtimekit
- Authentication handled by Clerk; backend verifies Clerk-issued JWTs and syncs users/orgs via webhook
- Authentication handled by Hazel auth (`packages/auth`); the RPC and HttpApi middlewares resolve the session cookie, a session bearer credential (native clients) or a bot token to `CurrentUser`
- Shared database package ensures type safety between frontend and backend
- Domain package (`packages/domain/`) contains shared contracts:
- RPC definitions for client-server communication
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/.artifacts/chat-sync/chat-sync-diagnostics.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
{"timestamp":"2026-06-23T01:00:51.380Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
{"timestamp":"2026-06-23T01:00:58.637Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"f280ff9c-f164-4a1f-974f-baa1d94a2911","expected":"synced","actual":"synced"}
{"timestamp":"2026-06-23T01:01:00.427Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"}
{"timestamp":"2026-10-15T13:52:47.847Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
{"timestamp":"2026-10-15T13:52:53.829Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"c52e9350-a742-464a-b6df-1722eb33157e","expected":"synced","actual":"synced"}
{"timestamp":"2026-10-15T13:52:55.547Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"}
9 changes: 3 additions & 6 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ DATABASE_URL=postgresql://user:password@localhost:5432/app
# Local cluster service (Docker)
CLUSTER_URL=http://localhost:3020

# Clerk Authentication
# Get these from: https://dashboard.clerk.com/ → API Keys
CLERK_SECRET_KEY=sk_test_your_clerk_secret_here
CLERK_PUBLISHABLE_KEY=pk_test_your_clerk_publishable_here
CLERK_WEBHOOK_SECRET=whsec_your_clerk_webhook_secret_here
# Optional: verifies Maple's Clerk-issued OAuth tokens for the public API v1 (the last Clerk use).
# CLERK_SECRET_KEY=sk_test_your_clerk_secret_here

# Hazel sign-in (replacing Clerk). Off until AUTH_GITHUB_CLIENT_ID is set.
# Hazel sign-in. Off until AUTH_GITHUB_CLIENT_ID is set.
# GitHub OAuth App callback URL: {API_BASE_URL}/auth/github/callback
# Google OAuth client redirect URI: {API_BASE_URL}/auth/google/callback
# Generate the keys with: bun run --cwd packages/auth generate-keys
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ See the [root README](../../README.md) for development setup instructions.
- **Effect-TS** for functional programming and dependency injection
- **Effect RPC** for type-safe APIs
- **Drizzle ORM** with PostgreSQL
- **Clerk** for authentication
- **Hazel auth** (`@hazel/auth`) for sign-in and sessions

## Development

Expand Down
15 changes: 4 additions & 11 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ import {
TypingIndicatorRepo,
UserPresenceStatusRepo,
UserRepo,
ClerkSync,
} from "@hazel/backend-core"
import { ClerkClient } from "@hazel/auth"
import { GitHub } from "@hazel/integrations"
import { Layer } from "effect"
import { HazelApi } from "./api"
import { HttpApiRoutes } from "./http"
import { ServerFailureReporterLive } from "./lib/error-reporter"
import { AttachmentPolicy } from "./policies/attachment-policy"
import { BotPolicy } from "./policies/bot-policy"
import { InvitationPolicy } from "./policies/invitation-policy"
Expand Down Expand Up @@ -81,7 +80,6 @@ import { ObjectStorage } from "./services/object-storage"
import { OAuthProviderRegistry } from "./services/oauth"
import { HazelAuthInstance, HazelAuthRoutes, HazelAuthServicesLive } from "./services/hazel-auth"
import { HazelSession } from "./services/hazel-session"
import { SessionManager } from "./services/session-manager"
import { WebhookBotService } from "./services/webhook-bot-service"
import { BotGatewayService } from "./services/bot-gateway-service"
import { ChannelAccessSyncService } from "./services/channel-access-sync"
Expand Down Expand Up @@ -129,6 +127,8 @@ export const AllRoutes = Layer.mergeAll(
credentials: true,
}),
),
// RpcServer and HttpApiBuilder report failed requests here; server failures get logged.
Layer.provide(ServerFailureReporterLive),
)

/**
Expand Down Expand Up @@ -208,8 +208,6 @@ export const AppServicesLive = Layer.mergeAll(
RepoLive,
PolicyLive,
MockDataGenerator.layer,
ClerkClient.layer,
ClerkSync.layer,
ObjectStorage.layer,
GitHub.GitHubAppJWTService.layer,
GitHub.GitHubApiClient.layer,
Expand All @@ -224,8 +222,6 @@ export const AppServicesLive = Layer.mergeAll(
ChannelAccessSyncService.layer,
ConnectConversationService.layer,
Invitations.layer,
// SessionManager.layer includes BackendAuth.layer via dependencies
SessionManager.layer,
OAuthBearerAuth.layer,
).pipe(Layer.provideMerge(FetchHttpClient.layer))

Expand All @@ -234,7 +230,4 @@ export const AppServicesLive = Layer.mergeAll(
* HTTP entry points build this; the Durable Objects never authenticate. Requires `Database`
* and the auth SQL connection from the platform.
*/
export const AppAuthorizationLive = AuthorizationLive.pipe(
Layer.provideMerge(SessionManager.layer),
Layer.provideMerge(HazelAuthLive),
)
export const AppAuthorizationLive = AuthorizationLive.pipe(Layer.provideMerge(HazelAuthLive))
13 changes: 3 additions & 10 deletions apps/backend/src/lib/create-transactionId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,10 @@ const TxidRow = Schema.Struct({ txid: Schema.String })
/**
* The id of the open transaction, for Electric to confirm the client's optimistic write against.
* Must run inside `db.transaction`, which pins this statement to the transaction's connection.
* Fails with the typed DatabaseError / SchemaError, which callers remap with `withRemapDbErrors`.
*/
export const generateTransactionId = Effect.fn("generateTransactionId")(function* () {
const db = yield* Database.Database
const rows = yield* db
.query(Database.sql`SELECT pg_current_xact_id()::xid::text AS txid`, TxidRow)
.pipe(
Effect.catchTag("DatabaseError", (err) =>
Effect.die(`Database error generating transaction ID: ${err}`),
),
)
return yield* Schema.decodeEffect(TransactionIdFromString)(rows[0]?.txid ?? "").pipe(
Effect.catchTag("SchemaError", (err) => Effect.die(`Failed to parse transaction ID: ${err}`)),
)
const rows = yield* db.query(Database.sql`SELECT pg_current_xact_id()::xid::text AS txid`, TxidRow)
return yield* Schema.decodeEffect(TransactionIdFromString)(rows[0]?.txid ?? "")
}, Database.requireTransaction)
60 changes: 60 additions & 0 deletions apps/backend/src/lib/error-reporter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { NodeHttpPlatform, NodeServices } from "@effect/platform-node"
import { describe, expect, it } from "@effect/vitest"
import { InternalServerError } from "@hazel/domain"
import { Cause, Context, Effect, Layer, Logger, Schema } from "effect"
import { Etag, HttpRouter } from "effect/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
import { ServerFailureReporterLive } from "./error-reporter"

class ProbeGroup extends HttpApiGroup.make("probe").add(
HttpApiEndpoint.get("boom", "/boom", { success: Schema.String, error: InternalServerError }),
) {}

const api = HttpApi.make("Probe").add(ProbeGroup)

const driverText = 'relation "users" does not exist'

const ProbeLive = HttpApiBuilder.group(api, "probe", (handlers) =>
handlers.handle("boom", () =>
Effect.fail(
new InternalServerError({ message: "Error selecting user", cause: new Error(driverText) }),
),
),
)

const serve = (logged: Array<string>) => {
const captureLogger = Logger.make<unknown, void>(({ cause }) => {
logged.push(Cause.pretty(cause))
})
const appLayer = HttpApiBuilder.layer(api).pipe(
Layer.provide(ProbeLive),
Layer.provide(ServerFailureReporterLive),
Layer.provide(Logger.layer([captureLogger])),
Layer.provideMerge(HttpRouter.layer),
Layer.provideMerge(Etag.layer),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(NodeHttpPlatform.layer),
)
return HttpRouter.toWebHandler(appLayer as never, { disableLogger: true })
}

describe("ServerFailureReporterLive", () => {
it("logs an InternalServerError with its cause, and keeps the cause out of the response", async () => {
const logged: Array<string> = []
const { handler, dispose } = serve(logged)
try {
const response = await handler(
new Request("http://localhost/boom"),
Context.empty() as Context.Context<any>,
)
const body = await response.text()

expect(response.status).toBe(500)
expect(body).toContain("Error selecting user")
expect(body).not.toContain(driverText)
expect(logged.some((line) => line.includes(driverText))).toBe(true)
} finally {
await dispose()
}
})
})
40 changes: 40 additions & 0 deletions apps/backend/src/lib/error-reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { type Cause, Effect, ErrorReporter, Predicate } from "effect"

/**
* Failures that mean the server, not the caller, went wrong. Their client-facing form is a
* stable message; the underlying `cause` (kept off the wire) is what this reporter logs.
* `SessionLoadError` is left out: the auth middlewares log its cause where they raise it.
*/
const SERVER_FAILURE_TAGS: ReadonlySet<string> = new Set([
"InternalServerError",
"WorkflowInitializationError",
"WorkflowServiceUnavailableError",
"UploadError",
// Escaped the remapping to InternalServerError; still a server failure.
"DatabaseError",
"SchemaError",
])

const isServerFailure = (reason: Cause.Reason<unknown>): boolean =>
reason._tag === "Die" ||
(reason._tag === "Fail" &&
Predicate.hasProperty(reason.error, "_tag") &&
typeof reason.error._tag === "string" &&
SERVER_FAILURE_TAGS.has(reason.error._tag))

/**
* Logs every RPC and HttpApi failure that is the server's fault (5xx tags and defects) with its
* full `Cause`, including the `cause` chain clients never see. Expected 4xx failures are not
* logged. Both RpcServer and HttpApiBuilder report failed requests to `CurrentErrorReporters`.
*/
const serverFailureReporter: ErrorReporter.ErrorReporter = {
[ErrorReporter.TypeId]: ErrorReporter.TypeId,
report: ({ cause, fiber }) => {
if (!cause.reasons.some(isServerFailure)) return
Effect.runSyncWith(fiber.context)(Effect.logError("Request failed", cause))
},
}

export const ServerFailureReporterLive = ErrorReporter.layer([serverFailureReporter], {
mergeWithExisting: true,
})
19 changes: 17 additions & 2 deletions apps/backend/src/lib/policy-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest"
import { UnauthorizedError } from "@hazel/domain"
import { InternalServerError, UnauthorizedError } from "@hazel/domain"
import { Effect, Result } from "effect"
import { makePolicy, withPolicyUnauthorized } from "./policy-utils.ts"
import { makeActor } from "../policies/policy-test-helpers.ts"
Expand Down Expand Up @@ -37,7 +37,7 @@ describe("policy-utils", () => {
it("maps non-unauthorized errors to UnauthorizedError", async () => {
const authorize = makePolicy("Widget")
const result = await Effect.runPromise(
authorize("read", () => Effect.fail({ _tag: "DatabaseError" as const })).pipe(
authorize("read", () => Effect.fail({ _tag: "EntityNotFound" as const })).pipe(
Effect.provideService(CurrentUser.Context, makeActor()),
Effect.result,
),
Expand All @@ -48,6 +48,21 @@ describe("policy-utils", () => {
expect(UnauthorizedError.is(result.failure)).toBe(true)
}
})

it("maps database failures to InternalServerError, never a 401", async () => {
const authorize = makePolicy("Widget")
const result = await Effect.runPromise(
authorize("read", () => Effect.fail({ _tag: "DatabaseError" as const })).pipe(
Effect.provideService(CurrentUser.Context, makeActor()),
Effect.result,
),
)

expect(Result.isFailure(result)).toBe(true)
if (Result.isFailure(result)) {
expect(InternalServerError.is(result.failure)).toBe(true)
}
})
})

describe("withPolicyUnauthorized", () => {
Expand Down
10 changes: 5 additions & 5 deletions apps/backend/src/routes/api-v1/integrations.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const HttpApiV1IntegrationsLive = HttpApiBuilder.group(HazelApi, "api-v1-
Effect.fail(
new InternalServerError({
message: "Failed to list organizations",
detail: String(err),
cause: err,
}),
),
),
Expand All @@ -120,7 +120,7 @@ export const HttpApiV1IntegrationsLive = HttpApiBuilder.group(HazelApi, "api-v1-
Effect.fail(
new InternalServerError({
message: "Failed to verify organization membership",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -149,7 +149,7 @@ export const HttpApiV1IntegrationsLive = HttpApiBuilder.group(HazelApi, "api-v1-
Effect.fail(
new InternalServerError({
message: "Failed to list channels",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -240,14 +240,14 @@ export const HttpApiV1IntegrationsLive = HttpApiBuilder.group(HazelApi, "api-v1-
Effect.fail(
new InternalServerError({
message: "Failed to create channel webhook",
detail: String(err),
cause: err,
}),
),
SchemaError: (err) =>
Effect.fail(
new InternalServerError({
message: "Schema validation failed",
detail: String(err),
cause: err,
}),
),
}),
Expand Down
10 changes: 5 additions & 5 deletions apps/backend/src/routes/api-v1/messages.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag
Effect.fail(
new InternalServerError({
message: "Database error while listing messages",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -291,7 +291,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag
Effect.fail(
new InternalServerError({
message: "Database error while creating message",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -358,7 +358,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag
Effect.fail(
new InternalServerError({
message: "Database error while updating message",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -427,7 +427,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag
Effect.fail(
new InternalServerError({
message: "Database error while deleting message",
detail: String(err),
cause: err,
}),
),
),
Expand Down Expand Up @@ -534,7 +534,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag
Effect.fail(
new InternalServerError({
message: "Database error while toggling reaction",
detail: String(err),
cause: err,
}),
),
),
Expand Down
Loading
Loading