Skip to content
Open
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
3 changes: 1 addition & 2 deletions e2e/mock-api-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"main": "./src/main.js",
"scripts": {
"build": "pnpm nx nxBuild",
"build": "pnpm nx build",
"dev": "node dist/src/main.js --watch-path=./",
"lint": "pnpm nx nxLint",
"serve": "node dist/src/main.js",
Expand All @@ -15,7 +15,6 @@
"dependencies": {
"@effect/language-service": "catalog:effect",
"@effect/opentelemetry": "catalog:effect",
"@effect/platform": "catalog:effect",
"@effect/platform-node": "catalog:effect",
"@opentelemetry/sdk-logs": "0.207.0",
"@opentelemetry/sdk-metrics": "2.2.0",
Expand Down
5 changes: 3 additions & 2 deletions e2e/mock-api-v2/src/handlers/authorize.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
*/
import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import { HttpApiBuilder, HttpApiError, HttpServerResponse } from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse';
import { getFirstElementAndRespond } from '../services/mock-env-helpers/index.js';

const AuthorizeHandlerMock = HttpApiBuilder.group(MockApi, 'Authorization', (handlers) =>
handlers.handle('authorize', ({ urlParams }) =>
handlers.handle('authorize', ({ query: urlParams }) =>
Effect.gen(function* () {
const acr_value = urlParams?.acr_values ?? '';

Expand Down
15 changes: 6 additions & 9 deletions e2e/mock-api-v2/src/handlers/capabilities.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
*/
import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import {
HttpApiBuilder,
HttpApiError,
HttpServerRequest,
HttpServerResponse,
} from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest';
import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse';
import { responseMap } from '../responses/index.js';
import { validator } from '../helpers/match.js';
import { returnSuccessResponseRedirect } from '../responses/return-success-redirect.js';
Expand Down Expand Up @@ -105,9 +102,9 @@ const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (h
},
),
),
Effect.flatMap((res) => HttpServerResponse.removeCookie(res, 'stepIndex')),
Effect.flatMap((res) => HttpServerResponse.setStatus(res, 200)),
Effect.flatMap((res) =>
Effect.map((res) => HttpServerResponse.removeCookie(res, 'stepIndex')),
Effect.map((res) => HttpServerResponse.setStatus(res, 200)),
Effect.map((res) =>
HttpServerResponse.setHeader(res, 'Content-Type', 'application/json'),
),
Effect.catchTag('CookieError', () => Effect.fail(new HttpApiError.InternalServerError())),
Expand Down
3 changes: 2 additions & 1 deletion e2e/mock-api-v2/src/handlers/end-session.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* of the MIT license. See the LICENSE file for details.
*/
import { Effect, Console } from 'effect';
import { HttpApiBuilder, HttpServerRequest } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest';
import { MockApi } from '../spec.js';
import { SessionStorage } from '../services/session.service.js';

Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/healthcheck.handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { MockApi } from '../spec.js';
import { Effect } from 'effect';

Expand Down
6 changes: 3 additions & 3 deletions e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
*/
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpServerRequest } from '@effect/platform/HttpServerRequest';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { HttpServerRequest } from 'effect/unstable/http/HttpServerRequest';

const OpenidConfigMock = HttpApiBuilder.group(MockApi, 'OpenIDConfig', (handlers) =>
handlers.handle('openid', ({ path: { envid } }) =>
handlers.handle('openid', ({ params: { envid } }) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url);
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/revoke.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { MockApi } from '../spec.js';
import { Tokens } from '../services/tokens.service.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { Effect } from 'effect';

const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'Revoke', (handlers) =>
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/token.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { MockApi } from '../spec.js';
import { Tokens } from '../services/tokens.service.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { Effect } from 'effect';

const TokensHandler = HttpApiBuilder.group(MockApi, 'Tokens', (handlers) =>
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/userinfo.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { UserInfo } from '../services/userinfo.service.js';
import { HttpApiBuilder, HttpApiError } from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import { BearerToken } from '../middleware/Authorization.js';

const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'ProtectedRequests', (handlers) =>
Expand Down
12 changes: 5 additions & 7 deletions e2e/mock-api-v2/src/helpers/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { Effect, Match, Schema } from 'effect';

import { HttpApiError } from '@effect/platform';
import { HttpApiError } from 'effect/unstable/httpapi';
import { CapabilitiesRequestBody } from '../schemas/capabilities/capabilities.request.schema.js';

type PingRequestData = Schema.Schema.Type<typeof CapabilitiesRequestBody>;
Expand All @@ -21,13 +21,11 @@ const validator = Match.type<PingRequestData>().pipe(
Match.when(
{ parameters: { data: { formData: { username: Match.string, password: Match.string } } } },
({ parameters }) =>
Effect.if(
Effect.suspend(() =>
parameters.data.formData.username == 'testuser' &&
parameters.data.formData.password === 'Password',
{
onFalse: () => Effect.fail(new HttpApiError.Unauthorized()),
onTrue: () => Effect.succeed(true),
},
parameters.data.formData.password === 'Password'
? Effect.succeed(true)
: Effect.fail(new HttpApiError.Unauthorized()),
),
),
Match.orElse(() => Effect.succeed(true)),
Expand Down
82 changes: 51 additions & 31 deletions e2e/mock-api-v2/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { Layer } from 'effect';
import { Effect, Layer } from 'effect';
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
import { MockApi } from './spec.js';
import { HttpApiBuilder, HttpApiSwagger, HttpMiddleware, HttpServer } from '@effect/platform';
import { HttpApiBuilder, HttpApiSwagger } from 'effect/unstable/httpapi';
import * as HttpMiddleware from 'effect/unstable/http/HttpMiddleware';
import * as HttpRouter from 'effect/unstable/http/HttpRouter';
import * as HttpServer from 'effect/unstable/http/HttpServer';
import type { ServeError } from 'effect/unstable/http/HttpServerError';
import { createServer } from 'node:http';
import { HealthCheckLive } from './handlers/healthcheck.handler.js';
import { OpenidConfigMock } from './handlers/open-id-configuration.handler.js';
Expand All @@ -26,47 +30,63 @@ import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trac
import { EndSessionHandlerMock } from './handlers/end-session.handler.js';
import { RevokeTokenHandler } from './handlers/revoke.handler.js';

const Services = [
Layer.provide(TokensMock),
Layer.provide(IncrementStepIndexMock),
Layer.provide(AuthorizationMock),
Layer.provide(UserInfoMockService),
Layer.provide(SessionMiddlewareMock),
Layer.provide(SessionStorage.Default),
] as const;

const NodeSdkLive = NodeSdk.layer(() => ({
resource: { serviceName: 'Mock-Api' },
spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
}));

const APIMock = HttpApiBuilder.api(MockApi).pipe(
Layer.provide(HealthCheckLive),
Layer.provide(OpenidConfigMock),
Layer.provide(AuthorizeHandlerMock),
Layer.provide(TokensHandler),
Layer.provide(CapabilitiesHandlerMock),
Layer.provide(UserInfoMockHandler),
Layer.provide(EndSessionHandlerMock),
Layer.provide(RevokeTokenHandler),
...Services,
// Wire SessionStorage into SessionMiddlewareMock
const SessionLayer = Layer.provide(
SessionMiddlewareMock,
Layer.effect(SessionStorage, SessionStorage.make),
);
Comment on lines +39 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline e2e/mock-api-v2/src/main.ts --items all

rg -n -C 6 'SessionStorage|SessionLayer|Layer\.provide|Layer\.provideMerge|Layer\.launch' \
  e2e/mock-api-v2/src/main.ts

rg -n -C 4 'yield\*\s+SessionStorage' \
  e2e/mock-api-v2/src/handlers --glob '*.ts'

Repository: ForgeRock/ping-javascript-sdk

Length of output: 5674


🌐 Web query:

effect 2.4 Layer.provideMerge Layer.provide outputs consumed semantics

💡 Result:

In Effect, Layer.provide and Layer.provideMerge are both combinators used for composing layers, but they differ in how they handle the resulting service environment [1][2][3]. Layer.provide Use Layer.provide when you want to satisfy a layer's internal dependencies using another layer without exposing those dependencies to the rest of the application [2][4]. Semantics: It wires the provided layer to satisfy the requirements of the base layer [4]. Once satisfied, those dependencies are "consumed" internally, meaning they are no longer available in the context of the resulting layer [2][4]. This is ideal for encapsulating implementation details [2]. Layer.provideMerge Use Layer.provideMerge when you want to satisfy a layer's dependencies while also keeping those dependencies available for use elsewhere in your program [4][3]. Semantics: It wires the provided layer to satisfy the requirements of the base layer, but crucially, it merges the output of the provided layer into the output of the resulting layer [2][3]. As a result, both the original services and the provided services remain accessible in the final context [4][3]. This is particularly common in testing scenarios where you need access to multiple services simultaneously [4]. Summary Comparison | Method | Deps Satisfied | Available to Program | Use When | |--------|---------------|---------------------|----------| | Layer.provide | Yes | No | Internal dependency satisfaction / Encapsulation [2][4] | | Layer.provideMerge | Yes | Yes | Exposing multiple services / Testing [4][3] | In essence, while both methods fulfill requirements, Layer.provide acts as a form of private dependency injection, whereas Layer.provideMerge acts as a union of available services [2][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "provideMerge|provide:" node_modules effect 2>/dev/null || true

Repository: ForgeRock/ping-javascript-sdk

Length of output: 167


Expose SessionStorage from SessionLayer.

Layer.provide satisfies SessionMiddlewareMock and consumes the SessionStorage output, so HandlersLayerServicesLayer has no provider for yield* SessionStorage in EndSessionHandlerMock. Use Layer.provideMerge here to satisfy the middleware and keep SessionStorage available to handlers.

Proposed fix
-const SessionLayer = Layer.provide(
+const SessionLayer = Layer.provideMerge(
   SessionMiddlewareMock,
   Layer.effect(SessionStorage, SessionStorage.make),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const SessionLayer = Layer.provide(
SessionMiddlewareMock,
Layer.effect(SessionStorage, SessionStorage.make),
);
const SessionLayer = Layer.provideMerge(
SessionMiddlewareMock,
Layer.effect(SessionStorage, SessionStorage.make),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/mock-api-v2/src/main.ts` around lines 39 - 42, Update the SessionLayer
definition to use Layer.provideMerge instead of Layer.provide, so
SessionMiddlewareMock is satisfied while SessionStorage remains exposed for
EndSessionHandlerMock through HandlersLayer and ServicesLayer.


// Merge all group handlers
const HandlersLayer = Layer.mergeAll(
HealthCheckLive,
OpenidConfigMock,
AuthorizeHandlerMock,
TokensHandler,
CapabilitiesHandlerMock,
UserInfoMockHandler,
EndSessionHandlerMock,
RevokeTokenHandler,
);

// Merge all services
const ServicesLayer = Layer.mergeAll(
TokensMock,
IncrementStepIndexMock,
AuthorizationMock,
UserInfoMockService,
SessionLayer,
);

// Build application routes layer with all handlers and services provided in one step each
const AppLayer = HttpApiBuilder.layer(MockApi).pipe(
Layer.provide(HandlersLayer),
Layer.provide(ServicesLayer),
);

// Compose app + swagger, then provide the router service
const AppWithSwagger = Layer.merge(AppLayer, HttpApiSwagger.layer(MockApi)).pipe(
Layer.provide(HttpRouter.layer),
);

const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiSwagger.layer()),
Layer.provide(
HttpApiBuilder.middlewareCors({
const ServerMock = HttpRouter.serve(AppWithSwagger, {
middleware: (app) =>
HttpMiddleware.cors({
allowedMethods: ['GET', 'PUT', 'POST', 'OPTIONS'],
allowedOrigins: ['*'],
credentials: true,
maxAge: 3600,
Comment on lines +78 to 82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In effect@4.0.0-beta.103, does HttpMiddleware.cors({ allowedOrigins: [""] })match arbitrary origins or only an Origin header equal to? Cite the source implementation or API reference.

💡 Result:

In Effect 4.0.0-beta.103, providing allowedOrigins: ["*"] in HttpMiddleware.cors() does not restrict access to origins that literally equal the string * [1][2]. Instead, the implementation treats this as a specific configuration that allows the asterisk character, which browsers interpret as a wildcard allowing all origins [1][2]. According to the official implementation and documentation, allowedOrigins can be an array of strings, a predicate function, or empty (which defaults to allowing all origins) [1][2]. If you provide ["*"], the middleware includes this in the Access-Control-Allow-Origin header for incoming requests, effectively enabling a wildcard CORS policy [3]. Source Implementation API Reference: The relevant implementation is found in the Effect Platform package's HttpMiddleware.ts file [1][2]. The cors constructor handles allowedOrigins by validating the incoming Origin header against the provided configuration (either by checking if the origin is included in the array or by executing the provided predicate) [1][3]. Relevant documentation excerpt from source: @param options.allowedOrigins - Origins allowed to access the resource. Can be: - An array of origin strings (e.g., ["https://example.com", "https://api.example.com"]) - A predicate function to dynamically allow origins - If empty array (default): allows all origins with Access-Control-Allow-Origin: * [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the affected file and inspect package versions / HttpMiddleware.cors usage.
printf 'Files:\n'
git ls-files | rg '(^|/)package.json$|e2e/mock-api-v2/src/main.ts|HttpMiddleware' || true

printf '\nAffected file context:\n'
sed -n '1,140p' e2e/mock-api-v2/src/main.ts

printf '\nRoot/package effect versions:\n'
node - <<'JS'
const fs = require('fs');
for (const p of ['package.json','package-lock.json','pnpm-lock.yaml','yarn.lock']) {
  if (fs.existsSync(p)) {
    console.log(`\n--- ${p} ---`);
    const s = fs.readFileSync(p, 'utf8');
    if (p.endsWith('.json')) {
      const j = JSON.parse(s);
      if (j.dependencies?.effect || j.devDependencies?.effect || j.dependencies?.'`@effect/platform`' || j.devDependencies?.'`@effect/platform`') {
        console.log(JSON.stringify({deps:j.dependencies, devDeps:j.devDependencies}, null, 2).slice(0, 2000));
      }
    } else {
      for (const m of ['effect','`@effect/platform`']) {
        const idx = s.indexOf(m + '@');
        console.log(m, idx >= 0 ? s.slice(idx, idx + 200) : 'not found');
      }
    }
  }
}
JS

printf '\nLockfile effect platform refs:\n'
rg -n '"?effect( |-|@)?/?platform"?|effect@.*4\.0\.0-beta\.103|HttpMiddleware\.cors|allowedOrigins' package* e2e -S || true

Repository: ForgeRock/ping-javascript-sdk

Length of output: 5774


🏁 Script executed:

#!/bin/bash
set -fu

printf 'Root/package effect version references:\n'
rg -n --no-heading '"?effect( |-|@)?/?platform"?|`@effect/platform`|effect@|HttpMiddleware\.cors|allowedOrigins' package.json e2e/mock-api-v2/package.json packages/*/package.json e2e/*/package.json -S || true

printf '\nPackage json versions if present:\n'
node - <<'JS'
const fs = require('fs');
const files = ['package.json','e2e/mock-api-v2/package.json'];
for (const f of files) {
  if (!fs.existsSync(f)) continue;
  const j = JSON.parse(fs.readFileSync(f, 'utf8'));
  console.log(`\n--- ${f} ---`);
  console.log(JSON.stringify({ dependencies: j.dependencies, devDependencies: j.devDependencies }, null, 2));
}
JS

printf '\nSource availability check for effect package files:\n'
for d in node_modules/effect node_modules/@effect/platform node_modules/cache?; do
  if [ -d "$d" ]; then echo "$d"; find "$d" -maxdepth 2 -type f | head -30; fi
done

Repository: ForgeRock/ping-javascript-sdk

Length of output: 3404


🌐 Web query:

Effect.ts unstable HttpMiddleware allowedOrigins "*" Origin header exact match

💡 Result:

In Effect.ts, the HttpMiddleware.cors implementation handles the allowedOrigins configuration by performing an exact match when an array of strings is provided [1][2]. Specifically, the middleware logic determines whether to allow an incoming Origin header based on the allowedOrigins option [1][2]: 1. If allowedOrigins is a ReadonlyArray<string>, the middleware checks if the incoming Origin header is included in that array [1][2]. 2. When the array contains exactly one origin, the middleware sets the Access-Control-Allow-Origin header to that specific origin [1][2]. 3. When the array contains multiple origins, or when a predicate function is provided, the middleware dynamically checks the Origin header and, if matched, reflects it back in the Access-Control-Allow-Origin header along with a Vary: Origin header [1][2]. 4. If allowedOrigins is not provided (or is an empty array), the middleware defaults to allowing all origins by setting Access-Control-Allow-Origin: * [3][1][2]. The use of Array.prototype.includes ensures that the comparison against the provided list of origins is an exact string match [1][2]. If you require more flexible matching (e.g., regex or suffix matching), you should provide a predicate function to the allowedOrigins option instead [3][4][5].

Citations:


🌐 Web query:

Effect.ts HttpMiddleware coredocs CORS allowedOrigins asterisk browser Origin header literal "*"

💡 Result:

In the Effect.ts platform library, the CORS middleware is configured via HttpMiddleware.cors [1][2]. According to the official documentation, if the allowedOrigins option is left as an empty array (which is the default behavior), the middleware will allow all origins by setting the Access-Control-Allow-Origin response header to the wildcard value * [2][3]. Regarding the browser's handling of the wildcard * in the Origin header or CORS response: 1. Functionality: The Access-Control-Allow-Origin: * header instructs the browser that any origin may access the resource, provided the request does not include credentials (such as cookies, authorization headers, or TLS client certificates) [4][5]. 2. Credentials Constraint: If a request includes credentials, the browser will block the response if the server returns Access-Control-Allow-Origin: * [5][6]. In such cases, the server must instead specify an explicit, single origin (e.g., Access-Control-Allow-Origin: https://example.com) [4][5][7]. 3. Security: Using the * wildcard is appropriate only for public, non-credentialed resources [8][6]. If your application requires credentialed access, you must configure allowedOrigins to a specific list or a predicate function to dynamically validate the Origin request header against your allowlist [1][2][5]. When using a dynamic predicate for allowedOrigins in Effect.ts, it is recommended practice to include the Vary: Origin response header to inform browsers that the response depends on the Origin request header [4][5].

Citations:


Configure trusted localhost origins instead of ['*'].

allowedOrigins: ['*'] only matches requests whose Origin header is literal *, so browser origins such as Vite development origins will not receive Access-Control-Allow-Origin. This disables browser CORS access when credentials: true. Use a configured test origin list or predicate instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/mock-api-v2/src/main.ts` around lines 78 - 82, Update the
HttpMiddleware.cors configuration to replace the wildcard allowedOrigins value
with the configured trusted localhost/test origin list or predicate, while
preserving credentials support and the existing allowedMethods and maxAge
settings.

}),
),
Layer.provide(APIMock),

Layer.provide(NodeSdkLive),
})(HttpMiddleware.logger(app)),
}).pipe(
HttpServer.withLogAddress,
Layer.provide(NodeSdkLive),
Layer.provide(NodeHttpServer.layer(createServer, { port: 9443, host: 'localhost' })),
);

Layer.launch(ServerMock).pipe(NodeRuntime.runMain);
// TypeScript cannot fully resolve complex Effect layer generic compositions;
// all requirements ARE satisfied at runtime — NodeHttpServer provides FileSystem, Path, HttpPlatform, Etag.
NodeRuntime.runMain(Layer.launch(ServerMock) as Effect.Effect<never, ServeError, never>);
33 changes: 21 additions & 12 deletions e2e/mock-api-v2/src/middleware/Authorization.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Unauthorized } from '@effect/platform/HttpApiError';
import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from '@effect/platform';
import { Brand, Context, Effect, Layer, Redacted } from 'effect';
import { HttpApiError, HttpApiMiddleware, HttpApiSecurity, OpenApi } from 'effect/unstable/httpapi';
import type { HttpServerResponse } from 'effect/unstable/http/HttpServerResponse';
import { Brand, Context, Effect, Layer, Redacted, Types } from 'effect';

type BearerTokenValue = string & Brand.Brand<'BearerToken'>;
const BearerTokenValue = Brand.nominal<BearerTokenValue>();

// Define a service that holds the bearer token
class BearerToken extends Context.Tag('BearerToken')<BearerToken, BearerTokenValue>() {}
class BearerToken extends Context.Service<BearerToken, BearerTokenValue>()('BearerToken') {}

class Authorization extends HttpApiMiddleware.Tag<Authorization>()('Authorization', {
failure: Unauthorized,
provides: BearerToken,
class Authorization extends HttpApiMiddleware.Service<
Authorization,
{ provides: typeof BearerToken }
>()('Authorization', {
error: HttpApiError.Unauthorized,
security: {
myBearer: HttpApiSecurity.bearer.pipe(
HttpApiSecurity.annotate(OpenApi.Description, 'Bearer token for API authentication'),
Expand All @@ -24,20 +26,27 @@ const AuthorizationMock = Layer.effect(
yield* Effect.log('creating Authorization middleware');

return {
myBearer: (bearerToken) =>
myBearer: (
httpEffect: Effect.Effect<HttpServerResponse, Types.unhandled, typeof BearerToken>,
{
credential,
}: { credential: Redacted.Redacted<string>; endpoint: unknown; group: unknown },
) =>
Effect.gen(function* () {
const tokenValue = Redacted.value(bearerToken);
const tokenValue = Redacted.value(credential);
yield* Effect.log('checking bearer token', tokenValue);
Comment on lines 35 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log the raw bearer token.

Redacted.value(credential) exposes the credential. Line 37 then writes it to logs for valid and invalid requests. Remove tokenValue from this log entry. Log only non-sensitive metadata.

Proposed fix
-          yield* Effect.log('checking bearer token', tokenValue);
+          yield* Effect.log('checking bearer token');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/mock-api-v2/src/middleware/Authorization.ts` around lines 35 - 37, Remove
the raw credential value from the logging flow in Authorization middleware: stop
deriving or passing tokenValue from Redacted.value(credential) to Effect.log,
and retain only the non-sensitive “checking bearer token” metadata for both
valid and invalid requests.


// Validation logic
// 1. Check if token is empty
// 2. Check if token has been revoked (has REVOKED_ prefix)
if (!tokenValue || tokenValue.trim() === '' || tokenValue.startsWith('REVOKED_')) {
return yield* Effect.fail(new Unauthorized());
return yield* Effect.fail(new HttpApiError.Unauthorized());
}

// Return the token value so routes can access it
return BearerTokenValue(tokenValue);
// Provide BearerToken and run the original effect
return yield* httpEffect.pipe(
Effect.provideService(BearerToken, BearerTokenValue(tokenValue)),
);
}),
};
}),
Expand Down
Loading
Loading