Skip to content

Commit 8a3d2d9

Browse files
committed
Authorize from the mirror unconditionally; alert on a stale reconciler
1 parent a6cdcf1 commit 8a3d2d9

29 files changed

Lines changed: 500 additions & 695 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@executor-js/cloud": patch
3+
---
4+
5+
`authorizeOrganization` now reads the local membership mirror unconditionally: the per-request readiness check (`MirrorReadiness`) and its live WorkOS `listUserMemberships` fallback are gone from the request path entirely. The backfill is complete and permanent, and an organization that predates the mirror is still covered by the existing on-demand scan (`ensureOrganizationBackfilled`). A stalled reconciler is now an operational alert instead of a per-request fallback: after each run, the cron checks the mirror's `drained_at` heartbeat and, if it has fallen behind the lag budget, logs a structured error and reports it to Sentry. The deploy gate (`scripts/ensure-workos-mirror-ready.ts`) is unchanged — it still refuses to ship while the mirror is unready — and `drained_at` keeps being written by every reconciler run.

‎.claude/skills/prod-telemetry/SKILL.md‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,33 @@ join the same traces via traceparent).
5757
`execute`/`execute-action` calls `mcp.execute.code` (the script itself,
5858
capped at 10k chars — cloud-only content capture; local/self-host
5959
telemetry never records content).
60-
- `auth.authorize_organization` — every membership authorization.
61-
`mirror.ready` (bool: the local membership mirror answered; `false` =
62-
the request fell back to a live WorkOS read) and `mirror.readiness`
63-
(why: `ready`, `backfill pending: …`, `reconciler stale: …`). The
64-
mirror's write spans are `workos_mirror.<op>`; the reconciler run is
60+
- `auth.authorize_organization` — every membership authorization. Reads the
61+
local membership mirror unconditionally; there is no per-request readiness
62+
check and no WorkOS fallback, so this span carries no readiness attribute.
63+
The mirror's write spans are `workos_mirror.<op>`; the reconciler run is
6564
`workos_events.sync`. `workos_sync.drained_at` in the prod DB is the
66-
reconciler heartbeat.
65+
reconciler heartbeat, and a stalled reconciler now raises its own error
66+
from the cron (see below) rather than showing up as a fallback here.
6767

68-
**Recipe — membership-mirror fallback rate (should be ~0 after cutover):**
68+
**Recipe — reconciler heartbeat (ticks should land roughly every minute; a
69+
gap wider than the 10-minute lag budget means the cron alert should already
70+
have fired — see `workos_events: reconciler stale` below):**
6971

7072
```apl
7173
['executor-cloud']
72-
| where _time > ago(1h) and name == "auth.authorize_organization"
73-
| extend ready = tobool(['attributes.custom']['mirror.ready'])
74-
| extend why = tostring(['attributes.custom']['mirror.readiness'])
75-
| summarize n = count() by ready, why
74+
| where _time > ago(1h) and name == "workos_events.sync"
75+
| summarize n = count() by bin(_time, 1m)
76+
| sort by _time desc
77+
```
78+
79+
**Recipe — stale-reconciler alerts (should be empty; each row is one paging
80+
event):**
81+
82+
```apl
83+
['executor-cloud']
84+
| where _time > ago(1d) and ['status.message'] contains "workos_events: reconciler stale"
85+
| project _time, trace_id, msg = tostring(['status.message'])
86+
| sort by _time desc
7687
```
7788

7889
**Recipe — error signatures by class (the daily-digest query):**

‎apps/cloud/src/account/account-api.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010

1111
import { ApiKeyService } from "../auth/api-keys";
1212
import { UserStoreService } from "../auth/context";
13-
import { MirrorReadiness } from "../auth/mirror-readiness";
1413
import { WorkOsMirror } from "../auth/workos-mirror";
1514
import { sessionFromSealed, type Session } from "../auth/middleware";
1615
import { WorkOSClient } from "../auth/workos";
@@ -51,7 +50,9 @@ import { AccountCaller, workosAccountProvider } from "./workos-account-service";
5150
// `UserStoreService` / `WorkOsMirror` / `MemberDirectory` are supplied by the
5251
// combined `rsLive` layer.
5352
// `ApiKeyService.WorkOS` is built here on top of the boot `WorkOSClient`.
54-
const AccountProviderMiddleware = HttpRouter.middleware<{ provides: AccountProvider }>()(
53+
const AccountProviderMiddleware = HttpRouter.middleware<{
54+
provides: AccountProvider;
55+
}>()(
5556
Effect.gen(function* () {
5657
// Long-lived services only (built once at boot). `UserStoreService` and
5758
// `DbService` are NOT grabbed here — they come per request from the combined
@@ -100,15 +101,11 @@ const AccountProviderMiddleware = HttpRouter.middleware<{ provides: AccountProvi
100101
* (the seat-gate) stays a residual requirement, satisfied by the app `boot`.
101102
*/
102103
export const workosAccountMiddleware = (
103-
rsLive: Layer.Layer<
104-
DbService | UserStoreService | WorkOsMirror | MemberDirectory | MirrorReadiness
105-
>,
104+
rsLive: Layer.Layer<DbService | UserStoreService | WorkOsMirror | MemberDirectory>,
106105
) => AccountProviderMiddleware.combine(requestScopedMiddleware(rsLive)).layer;
107106

108107
export const makeAccountApiLive = (
109-
rsLive: Layer.Layer<
110-
DbService | UserStoreService | WorkOsMirror | MemberDirectory | MirrorReadiness
111-
>,
108+
rsLive: Layer.Layer<DbService | UserStoreService | WorkOsMirror | MemberDirectory>,
112109
) => {
113110
// Cloud builds the WorkOS `AccountProvider` INSIDE the request body (so it
114111
// closes over the per-request postgres socket), so it can't be a self-

‎apps/cloud/src/account/org-api-key-revoke.node.test.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { AccountError, AccountForbidden } from "@executor-js/api";
66

77
import { ApiKeyService, OrgApiKeyNotFound } from "../auth/api-keys";
88
import { UserStoreService } from "../auth/context";
9-
import { MirrorReadiness, MirrorReadinessState } from "../auth/mirror-readiness";
109
import { ORG_SELECTOR_HEADER } from "../auth/organization";
1110
import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
1211
import { WorkOsMirror } from "../auth/workos-mirror";
@@ -132,12 +131,6 @@ const stubMirror = Layer.succeed(WorkOsMirror)({
132131
// The mirror as the directory reads it: both are active members of ORG, and
133132
// only ADMIN carries the `admin` role. Revoke reads the caller's membership
134133
// (the org check and the admin gate) and nothing else.
135-
// The mirror is READY in these tests (backfill complete, reconciler caught
136-
// up), so membership is read from the stubbed directory, never from WorkOS.
137-
const stubReadiness = Layer.succeed(MirrorReadiness)({
138-
state: () => Effect.succeed(MirrorReadinessState.Ready()),
139-
});
140-
141134
const stubDirectory = Layer.succeed(MemberDirectory)({
142135
membership: (accountId, organizationId) =>
143136
Effect.succeed(
@@ -201,7 +194,6 @@ const providerWith = (accountId: string) => {
201194
stubUsers,
202195
stubMirror,
203196
stubDirectory,
204-
stubReadiness,
205197
stubApiKeys,
206198
stubAutumn,
207199
Layer.succeed(AccountCaller)({ session: session(accountId) }),

‎apps/cloud/src/account/workos-account-service.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import { ApiKeyService } from "../auth/api-keys";
1212
import { UserStoreService } from "../auth/context";
1313
import type { Session } from "../auth/middleware";
14-
import { MirrorReadiness } from "../auth/mirror-readiness";
1514
import { WorkOSClient } from "../auth/workos";
1615
import { ensureOrganizationBackfilled, mirrorInvitedMember } from "../auth/mirror-feeders";
1716
import { WorkOsMirror, mirrorMembershipFromWorkOs } from "../auth/workos-mirror";
@@ -72,7 +71,6 @@ export const workosAccountProvider: Layer.Layer<
7271
| UserStoreService
7372
| WorkOsMirror
7473
| MemberDirectory
75-
| MirrorReadiness
7674
| ApiKeyService
7775
| AutumnService
7876
| AccountCaller
@@ -103,12 +101,7 @@ export const workosAccountProvider: Layer.Layer<
103101
// erased to `R = never`, as the neutral AccountProvider shape requires.
104102
// Provided per method below.
105103
const ctx = yield* Effect.context<
106-
| WorkOSClient
107-
| UserStoreService
108-
| AutumnService
109-
| MemberDirectory
110-
| MirrorReadiness
111-
| WorkOsMirror
104+
WorkOSClient | UserStoreService | AutumnService | MemberDirectory | WorkOsMirror
112105
>();
113106

114107
// Unauthenticated (missing/invalid session) => AccountUnauthorized, exactly

‎apps/cloud/src/admin/admin-users-api.node.test.ts‎

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { MemberDirectory, type DirectoryMember } from "@executor-js/api/server";
66

77
import { ApiKeyService } from "../auth/api-keys";
88
import { UserStoreService } from "../auth/context";
9-
import { MirrorReadiness, MirrorReadinessState } from "../auth/mirror-readiness";
109
import { ORG_SELECTOR_HEADER } from "../auth/organization";
1110
import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
1211
import { WorkOsMirror, type WorkOsMirrorShape } from "../auth/workos-mirror";
@@ -49,12 +48,6 @@ const memberships = new Map<string, DirectoryMember>([
4948
["user_invited_admin", mirrored("user_invited_admin", { role: "admin", status: "pending" })],
5049
]);
5150

52-
// The mirror is READY in these tests (backfill complete, reconciler caught
53-
// up), so membership is read from the stubbed directory, never from WorkOS.
54-
const stubReadiness = Layer.succeed(MirrorReadiness)({
55-
state: () => Effect.succeed(MirrorReadinessState.Ready()),
56-
});
57-
5851
const stubDirectory = Layer.succeed(MemberDirectory)({
5952
membership: (accountId, organizationId) =>
6053
Effect.succeed(organizationId === ORG ? (memberships.get(accountId) ?? null) : null),
@@ -149,7 +142,11 @@ const stubWorkOS = (userId: string) =>
149142
get: (_target, prop) => {
150143
if (prop === "authenticateRequest") {
151144
return () =>
152-
Effect.succeed({ userId, email: `${userId}@placeholder.test`, organizationId: null });
145+
Effect.succeed({
146+
userId,
147+
email: `${userId}@placeholder.test`,
148+
organizationId: null,
149+
});
153150
}
154151
return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
155152
},
@@ -163,14 +160,7 @@ const authorizeAs = (userId: string) =>
163160
}),
164161
).pipe(
165162
Effect.provide(
166-
Layer.mergeAll(
167-
stubDirectory,
168-
stubApiKeys,
169-
stubUsers,
170-
stubWorkOS(userId),
171-
stubMirror,
172-
stubReadiness,
173-
),
163+
Layer.mergeAll(stubDirectory, stubApiKeys, stubUsers, stubWorkOS(userId), stubMirror),
174164
),
175165
);
176166

‎apps/cloud/src/admin/admin-users-api.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import type { Executor } from "@executor-js/sdk";
5656

5757
import { ApiKeyService } from "../auth/api-keys";
5858
import { UserStoreService } from "../auth/context";
59-
import { MirrorReadiness } from "../auth/mirror-readiness";
6059
import { WorkOsMirror } from "../auth/workos-mirror";
6160
import { isPlatformAuth, resolveBearerAuth } from "../auth/workos-auth-provider";
6261
import { orgSelectorFromRequest, authorizeOrganizationSelector } from "../auth/organization";
@@ -77,7 +76,7 @@ export const authorizeTenant = (
7776
): Effect.Effect<
7877
string,
7978
AdminUsersUnauthorized | AdminUsersForbidden,
80-
WorkOSClient | ApiKeyService | UserStoreService | MemberDirectory | MirrorReadiness | WorkOsMirror
79+
WorkOSClient | ApiKeyService | UserStoreService | MemberDirectory | WorkOsMirror
8180
> =>
8281
Effect.gen(function* () {
8382
// (1) The bearer path. `resolveBearerAuth` (not `resolveApiKeyPrincipal`,
@@ -138,7 +137,6 @@ const withPlatformView = <A, E extends AdminUsersError | AdminUserNotFound = Adm
138137
| ApiKeyService
139138
| UserStoreService
140139
| MemberDirectory
141-
| MirrorReadiness
142140
| WorkOsMirror
143141
| DbProvider
144142
| PluginsProvider
@@ -178,7 +176,6 @@ export const workosAdminUsersProvider: Layer.Layer<
178176
| ApiKeyService
179177
| UserStoreService
180178
| MemberDirectory
181-
| MirrorReadiness
182179
| WorkOsMirror
183180
| DbProvider
184181
| PluginsProvider
@@ -190,7 +187,6 @@ export const workosAdminUsersProvider: Layer.Layer<
190187
| ApiKeyService
191188
| UserStoreService
192189
| MemberDirectory
193-
| MirrorReadiness
194190
| WorkOsMirror
195191
| DbProvider
196192
| PluginsProvider
@@ -241,7 +237,9 @@ export const workosAdminUsersProvider: Layer.Layer<
241237
// per-request `DbService`/`UserStoreService`/`MemberDirectory` (and the
242238
// execution seams built over them) are supplied by the combined
243239
// `requestScopedMiddleware`.
244-
const AdminUsersProviderMiddleware = HttpRouter.middleware<{ provides: AdminUsersProvider }>()(
240+
const AdminUsersProviderMiddleware = HttpRouter.middleware<{
241+
provides: AdminUsersProvider;
242+
}>()(
245243
Effect.gen(function* () {
246244
const longLived = yield* Effect.context<WorkOSClient | ApiKeyService>();
247245
return (httpEffect) =>
@@ -265,9 +263,7 @@ const AdminUsersProviderMiddleware = HttpRouter.middleware<{ provides: AdminUser
265263
* `/api` prefix as the rest of the cloud router.
266264
*/
267265
export const makeCloudAdminUsersRoutes = (
268-
rsLive: Layer.Layer<
269-
DbService | UserStoreService | MemberDirectory | MirrorReadiness | WorkOsMirror
270-
>,
266+
rsLive: Layer.Layer<DbService | UserStoreService | MemberDirectory | WorkOsMirror>,
271267
options: Parameters<typeof makeAdminUsersApiLayer>[1] = {},
272268
) =>
273269
makeAdminUsersApiLayer(

‎apps/cloud/src/api/layers.ts‎

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import { SessionAuthLive } from "../auth/middleware-live";
1212
import { UserStoreService } from "../auth/context";
1313
import { cloudMemberDirectoryLayer } from "../auth/member-directory";
14-
import { MirrorReadiness } from "../auth/mirror-readiness";
1514
import { WorkOsMirror } from "../auth/workos-mirror";
1615
import {
1716
CloudAuthPublicHandlers,
@@ -36,23 +35,14 @@ const WorkOsMirrorLive = WorkOsMirror.Live.pipe(Layer.provide(DbLive));
3635
// The shared `MemberDirectory` read seam over the membership mirror — the
3736
// same per-request socket the mirror writes through.
3837
const MemberDirectoryLive = cloudMemberDirectoryLayer.pipe(Layer.provide(DbLive));
39-
// Whether the mirror may authorize this request at all (backfill complete,
40-
// reconciler caught up) — read on the same socket before the membership row.
41-
const MirrorReadinessLive = MirrorReadiness.Live.pipe(Layer.provide(DbLive));
4238

4339
// Per-request layer. Anything that opens an I/O object (postgres.js socket,
4440
// fetch stream readers, anything backed by a `Writable`) MUST live here —
4541
// `provideRequestScoped` rebuilds it per request so Cloudflare Workers'
4642
// I/O isolation is satisfied. See `api.request-scope.test.ts`.
4743
export const RequestScopedServicesLive: Layer.Layer<
48-
DbService | UserStoreService | WorkOsMirror | MemberDirectory | MirrorReadiness
49-
> = Layer.mergeAll(
50-
DbLive,
51-
UserStoreLive,
52-
WorkOsMirrorLive,
53-
MemberDirectoryLive,
54-
MirrorReadinessLive,
55-
);
44+
DbService | UserStoreService | WorkOsMirror | MemberDirectory
45+
> = Layer.mergeAll(DbLive, UserStoreLive, WorkOsMirrorLive, MemberDirectoryLive);
5646

5747
// Boot-scoped layer. Built once at worker boot, reused across requests.
5848
// Safe for config, in-memory caches, the global tracer provider, and
@@ -77,9 +67,7 @@ export const BootSharedServices = Layer.mergeAll(
7767
// handler reads it for the free-organizations-per-user limit gate — one of the
7868
// few app-only billing touchpoints. (It is NOT on the neutral boot core.)
7969
export const makeNonProtectedApiLive = (
80-
rsLive: Layer.Layer<
81-
DbService | UserStoreService | WorkOsMirror | MemberDirectory | MirrorReadiness
82-
>,
70+
rsLive: Layer.Layer<DbService | UserStoreService | WorkOsMirror | MemberDirectory>,
8371
) =>
8472
HttpApiBuilder.layer(NonProtectedApi).pipe(
8573
Layer.provide(Layer.mergeAll(CloudAuthPublicHandlers, CloudSessionAuthHandlers)),
@@ -95,9 +83,7 @@ export const makeNonProtectedApiLive = (
9583
// gates on billing, so `AutumnService.Default` is provided here, not on the
9684
// neutral boot core.
9785
export const makeOrgApiLive = (
98-
rsLive: Layer.Layer<
99-
DbService | UserStoreService | MemberDirectory | MirrorReadiness | WorkOsMirror
100-
>,
86+
rsLive: Layer.Layer<DbService | UserStoreService | MemberDirectory | WorkOsMirror>,
10187
) =>
10288
HttpApiBuilder.layer(OrgHttpApi).pipe(
10389
Layer.provide(OrgHandlers),
@@ -143,7 +129,9 @@ export const OrgApiLive = makeOrgApiLive(RequestScopedServicesLive);
143129
// folded into `.layer` here; the rest of the router (`makeApiLive` in
144130
// `./router.ts`, `./protected.ts`, the test harness) re-provides the same
145131
// shared `RouterConfigLive` directly.
146-
const protectedApi = makeProtectedApiLayer(cloudPlugins, { errorCapture: ErrorCaptureLive });
132+
const protectedApi = makeProtectedApiLayer(cloudPlugins, {
133+
errorCapture: ErrorCaptureLive,
134+
});
147135

148136
export const ProtectedCloudApi = protectedApi.api;
149137
export const ProtectedCloudApiHandlers = protectedApi.handlers;

‎apps/cloud/src/api/protected-api-key-auth.node.test.ts‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { MemberDirectory } from "@executor-js/api/server";
55

66
import { ApiKeyService } from "../auth/api-keys";
77
import { UserStoreService } from "../auth/context";
8-
import { MirrorReadiness, MirrorReadinessState } from "../auth/mirror-readiness";
98
import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
109
import { WorkOsMirror, type WorkOsMirrorShape } from "../auth/workos-mirror";
1110
import { resolveProtectedPrincipal } from "./protected";
@@ -55,13 +54,8 @@ const stubWorkOS = Layer.succeed(
5554
);
5655

5756
// The mirror as the directory reads it: user_123 holds an active membership in
58-
// org_123 and nothing else. Membership is never read from WorkOS.
59-
// The mirror is READY in these tests (backfill complete, reconciler caught
60-
// up), so membership is read from the stubbed directory, never from WorkOS.
61-
const stubReadiness = Layer.succeed(MirrorReadiness)({
62-
state: () => Effect.succeed(MirrorReadinessState.Ready()),
63-
});
64-
57+
// org_123 and nothing else. Membership is always read from the mirror, never
58+
// from WorkOS.
6559
const stubDirectory = Layer.succeed(MemberDirectory)({
6660
membership: (accountId, organizationId) =>
6761
Effect.succeed(
@@ -136,9 +130,7 @@ const stubMirror = Layer.succeed(
136130

137131
const run = (request: Request) =>
138132
resolveProtectedPrincipal(request).pipe(
139-
Effect.provide(
140-
Layer.mergeAll(stubApiKeys, stubWorkOS, stubUsers, stubDirectory, stubMirror, stubReadiness),
141-
),
133+
Effect.provide(Layer.mergeAll(stubApiKeys, stubWorkOS, stubUsers, stubDirectory, stubMirror)),
142134
);
143135

144136
describe("protected API key auth", () => {

‎apps/cloud/src/api/protected-jwt-auth.node.test.ts‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { MemberDirectory } from "@executor-js/api/server";
66

77
import { ApiKeyService } from "../auth/api-keys";
88
import { UserStoreService } from "../auth/context";
9-
import { MirrorReadiness, MirrorReadinessState } from "../auth/mirror-readiness";
109
import type { JwtBearerConfig } from "../auth/workos-auth-provider";
1110
import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
1211
import { WorkOsMirror, type WorkOsMirrorShape } from "../auth/workos-mirror";
@@ -72,13 +71,8 @@ const stubWorkOS = Layer.succeed(
7271
);
7372

7473
// The mirror as the directory reads it: user_123 holds an active membership in
75-
// org_123 and nothing else. Membership is never read from WorkOS.
76-
// The mirror is READY in these tests (backfill complete, reconciler caught
77-
// up), so membership is read from the stubbed directory, never from WorkOS.
78-
const stubReadiness = Layer.succeed(MirrorReadiness)({
79-
state: () => Effect.succeed(MirrorReadinessState.Ready()),
80-
});
81-
74+
// org_123 and nothing else. Membership is always read from the mirror, never
75+
// from WorkOS.
8276
const stubDirectory = Layer.succeed(MemberDirectory)({
8377
membership: (accountId, organizationId) =>
8478
Effect.succeed(
@@ -153,9 +147,7 @@ const stubMirror = Layer.succeed(
153147

154148
const run = (request: Request, jwt: JwtBearerConfig) =>
155149
resolveProtectedPrincipal(request, jwt).pipe(
156-
Effect.provide(
157-
Layer.mergeAll(stubApiKeys, stubWorkOS, stubUsers, stubDirectory, stubMirror, stubReadiness),
158-
),
150+
Effect.provide(Layer.mergeAll(stubApiKeys, stubWorkOS, stubUsers, stubDirectory, stubMirror)),
159151
);
160152

161153
const request = (token: string) =>

0 commit comments

Comments
 (0)