Skip to content

Adding portless and refactoring mock service packages#185

Open
nnoce14 wants to merge 35 commits intomainfrom
nnoce14/issue174
Open

Adding portless and refactoring mock service packages#185
nnoce14 wants to merge 35 commits intomainfrom
nnoce14/issue174

Conversation

@nnoce14
Copy link
Copy Markdown
Member

@nnoce14 nnoce14 commented Apr 2, 2026

Summary by Sourcery

Adopt portless-based HTTPS local development URLs, refactor mock services into seedwork + app-level servers, and improve frontend auth handling and docs to match the new dev workflow.

New Features:

  • Introduce reusable seedwork packages for OAuth2/OIDC and MongoDB in-memory replica set mock servers, consumed by dedicated app-level mock service packages.
  • Add new app-level mock OAuth2 server and MongoDB memory server applications to run as first-class dev targets in the monorepo.
  • Add portless-powered dev entry points for API, UI, docs, and mock services, providing stable HTTPS ownercommunity.localhost domains.

Enhancements:

  • Refine the RequireAuth UI component to better handle auth callbacks, display errors, and show explicit loading/redirect states instead of immediately forcing redirects.
  • Simplify mock service seedwork packages by exposing programmatic startup APIs and relying on app packages for environment configuration and process orchestration.
  • Adjust Vite and Azure Functions dev startup to cooperate with portless, including dedicated start-dev scripts and conditional local port usage.
  • Update SonarQube configuration to track the new seedwork packages and exclude the new mock-service paths from source and coverage where appropriate.

Documentation:

  • Revise local development documentation and root README to describe the portless-based HTTPS workflow, new dev commands, and updated local service URLs.
  • Add an ADR documenting the decision to adopt portless for local development, including rationale, trade-offs, and future evolution.
  • Document the new OAuth2 mock server app and OAuth2 seedwork package, including configuration, endpoints, and integration guidance.
  • Update MongoDB memory server documentation to describe its role as reusable seedwork behind the new app-level mock service.

nnoce14 added 30 commits March 30, 2026 20:03
…t.config.ts and shared config, update test description in RequireAuth tests
…h tests and adjust biome version in lockfile to fix pipeline
… vitest type checking for test files without polluting production output with test files and dev dependencies

- Updated sidebars.ts to use ES module syntax for exporting sidebars.
- Modified vitest.config.ts files across multiple packages to disable type checking.
- Added tsconfig.vitest.json files to various packages and apps to extend base TypeScript configurations for Vitest.
- Enhanced test files to use Mock types for better type safety and clarity.
- Adjusted test implementations to ensure proper async handling and error propagation.
- Updated turbo.json to streamline build dependencies and inputs.
…omImportMetaUrl utility and updating related imports according to sourcery suggestions
… for optional fields handling according to sourcery suggestion
…ases; add snyk exceptions with justification
… test processes; upgrade turbo to version 2.9.2
…nd script clarity

chore: update sonar-project.properties to reflect new mock service package names

chore: modify turbo.json to include a new dev task configuration
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 2, 2026

Reviewer's Guide

Refactors local mock services into reusable seedwork packages and dedicated apps, adopts portless-based HTTPS local development with stable ownercommunity.localhost subdomains, and hardens auth UX/flows while updating configs, scripts, and docs accordingly.

Sequence diagram for RequireAuth and mock OIDC login via portless

sequenceDiagram
  actor User
  participant Browser
  participant RequireAuth
  participant OIDCClient
  participant MockAuthServer as Mock_OIDC_Server

  User->>Browser: Navigate to protected route
  Browser->>RequireAuth: Render component
  RequireAuth->>RequireAuth: useAuth() state check
  alt NotAuthenticated and notHandlingCallback and forceLogin
    RequireAuth->>Browser: sessionStorage.setItem("redirectTo")
    RequireAuth->>OIDCClient: signinRedirect()
    RequireAuth-->>Browser: Show "Redirecting to sign in..." view
    OIDCClient->>MockAuthServer: GET https://mock-auth.ownercommunity.localhost/authorize
    MockAuthServer-->>OIDCClient: 302 redirect with code,state to https://ownercommunity.localhost/auth-redirect
    OIDCClient-->>Browser: Navigate to /auth-redirect with code,state
  end

  Browser->>RequireAuth: Re-render on /auth-redirect
  RequireAuth->>RequireAuth: hasAuthParams() == true
  RequireAuth-->>Browser: Show loading view (includes auth callback state)
  OIDCClient->>MockAuthServer: POST /token (exchange code)
  MockAuthServer-->>OIDCClient: id_token, access_token, refresh_token
  OIDCClient-->>RequireAuth: auth.isAuthenticated = true

  alt AuthenticationSuccess
    RequireAuth-->>Browser: Render children (protected UI)
  else AuthenticationError
    RequireAuth-->>Browser: Render error view with auth.error.message
  end
Loading

Class diagram for new OAuth2 and MongoDB mock seedwork and apps

classDiagram
  class MockOAuth2UserProfile {
    <<interface>>
    +string email
    +string given_name
    +string family_name
    +string sub
    +string tid
  }

  class MockOAuth2ServerConfig {
    <<interface>>
    +number port
    +string baseUrl
    +string host
    +Set~string~ allowedRedirectUris
    +string allowedRedirectUri
    +Map~string,string~ redirectUriToAudience
    +getUserProfile() MockOAuth2UserProfile
  }

  class startMockOAuth2Server {
    +startMockOAuth2Server(config MockOAuth2ServerConfig) Promise~void~
  }

  class MongoMemoryReplicaSetConfig {
    <<interface>>
    +number port
    +string dbName
    +string replSetName
    +string binaryVersion
  }

  class startMongoMemoryReplicaSet {
    +startMongoMemoryReplicaSet(config MongoMemoryReplicaSetConfig) Promise~MongoMemoryReplSet~
  }

  class ServerOAuth2MockApp {
    <<module @apps/server-oauth2-mock>>
    +setupEnvironment() void
    +main() void
  }

  class ServerMongoMemoryMockApp {
    <<module @apps/server-mongodb-memory-mock>>
    +setupEnvironment() void
    +main() void
  }

  class RequireAuthProps {
    <<interface>>
    +boolean forceLogin
    +ReactNode children
  }

  class RequireAuth {
    <<React.FC>>
    +RequireAuth(props RequireAuthProps) ReactElement
  }

  MockOAuth2ServerConfig --> MockOAuth2UserProfile : uses
  startMockOAuth2Server --> MockOAuth2ServerConfig : parameter

  startMongoMemoryReplicaSet --> MongoMemoryReplicaSetConfig : parameter

  ServerOAuth2MockApp --> MockOAuth2ServerConfig : builds
  ServerOAuth2MockApp --> startMockOAuth2Server : calls

  ServerMongoMemoryMockApp --> MongoMemoryReplicaSetConfig : builds
  ServerMongoMemoryMockApp --> startMongoMemoryReplicaSet : calls

  RequireAuth --> RequireAuthProps : uses
Loading

File-Level Changes

Change Details Files
Introduce reusable seedwork packages for MongoDB memory replica sets and OAuth2/OIDC mock servers and consume them from new app-level mock services.
  • Rename and reshape the MongoDB memory mock package into a seedwork library that exposes a programmatic replica-set startup API instead of a CLI runner.
  • Add a new OAuth2/OIDC mock seedwork package that encapsulates token issuance, JWKS, discovery, authorize, token, userinfo, and logout endpoints using Express and jose.
  • Create dedicated server-mongodb-memory-mock and server-oauth2-mock apps that wire environment loading, configuration, and dev/start scripts (including portless integration for the OAuth app) on top of the seedwork packages.
  • Document the new mock apps and seedwork responsibilities, configuration, and integration points with API and UI.
packages/cellix/server-mongodb-memory-mock-seedwork/README.md
packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts
packages/cellix/server-oauth2-mock-seedwork/package.json
packages/cellix/server-oauth2-mock-seedwork/README.md
packages/cellix/server-oauth2-mock-seedwork/src/index.ts
apps/server-mongodb-memory-mock/package.json
apps/server-mongodb-memory-mock/tsconfig.json
apps/server-mongodb-memory-mock/src/index.ts
apps/server-mongodb-memory-mock/src/setup-environment.ts
apps/server-oauth2-mock/package.json
apps/server-oauth2-mock/tsconfig.json
apps/server-oauth2-mock/src/index.ts
apps/server-oauth2-mock/src/setup-environment.ts
apps/server-oauth2-mock/README.md
Adopt portless as the local HTTPS proxy and update app-level dev workflows, URLs, and OIDC configuration to use stable ownercommunity.localhost domains.
  • Wrap root dev script with portless proxy lifecycle management and add portless as a dev dependency.
  • Add per-app dev entrypoints for API, UI, and docs that run through portless, including new start-dev.mjs shims for Functions and Docusaurus.
  • Adjust the UI Vite dev server to run bare only when PORTLESS_URL is absent, allowing seamless cooperation with portless.
  • Change default OIDC client authority/redirect URIs in the UI to use mock-auth.ownercommunity.localhost and ownercommunity.localhost URLs.
  • Update docs and root README to describe the new portless-based dev flow, HTTPS URLs, and per-app dev commands, and add an ADR documenting the portless adoption and local domain model.
package.json
apps/api/package.json
apps/api/start-dev.mjs
apps/ui-community/package.json
apps/ui-community/vite.config.ts
apps/ui-community/src/config/oidc-config.tsx
apps/docs/package.json
apps/docs/start-dev.mjs
apps/docs/docs/intro.md
apps/docs/docs/decisions/0028-portless-local-development.md
readme.md
Improve RequireAuth behavior and error handling to avoid redirect loops during auth callbacks and provide clearer UX around loading, redirecting, and failures.
  • Cache hasAuthParams result and use it to guard the auto sign-in effect, preventing redirects while handling callback parameters.
  • Treat auth-callback handling as a loading state alongside isLoading/activeNavigator to avoid rendering protected content prematurely.
  • Add an explicit error view that surfaces authentication failure messages instead of silently navigating away.
  • Replace immediate signinRedirect calls in the render path with a user-visible "Redirecting to sign in" loading screen when forceLogin triggers.
  • Ensure forceLogin=false flows redirect to home without implicit sign-in attempts.
packages/cellix/ui-core/src/components/molecules/require-auth/index.tsx
Update quality and build tooling configuration to align with the new package layout and dev model.
  • Point SonarQube source inclusions at the new seedwork packages and move exclusions off the deprecated mock-* packages.
  • Keep seedwork packages excluded from coverage as non-critical dev/testing infrastructure code.
  • Tweak Turbo and lockfile as needed to reflect new apps/packages and scripts (implicit from modified files).
sonar-project.properties
turbo.json
pnpm-lock.yaml
Remove or replace deprecated mock service packages and environment files in favor of the new seedwork + app structure.
  • Delete the old mock OAuth2 and MongoDB memory server implementation source, READMEs, env, and turbo config under packages/cellix/mock-*.
  • Move MongoDB mock env into the new app-level package and add ignore files where appropriate.
  • Ensure references in docs and scripts point only to the new structure and commands.
packages/cellix/mock-mongodb-memory-server/src/index.ts
packages/cellix/mock-mongodb-memory-server/src/setup-environment.ts
packages/cellix/mock-oauth2-server/.env
packages/cellix/mock-oauth2-server/README.md
packages/cellix/mock-oauth2-server/src/index.ts
packages/cellix/mock-oauth2-server/src/setup-environment.ts
packages/cellix/mock-oauth2-server/turbo.json
apps/server-mongodb-memory-mock/.env
apps/server-mongodb-memory-mock/.gitignore
apps/server-oauth2-mock/.env
apps/server-oauth2-mock/.gitignore

Possibly linked issues

  • #Implement portless local development with custom domains and align mock service startup in @apps: PR implements portless-based dev, ownercommunity.localhost domains, app-level mock services, and documentation/config changes requested in the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Base automatically changed from nnoce14/issue179 to main April 2, 2026 20:21
@nnoce14 nnoce14 marked this pull request as ready for review April 2, 2026 20:24
@nnoce14 nnoce14 requested a review from a team April 2, 2026 20:24
@nnoce14 nnoce14 requested a review from a team as a code owner April 2, 2026 20:24
@nnoce14
Copy link
Copy Markdown
Member Author

nnoce14 commented Apr 2, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 7 issues, and left some high level feedback:

  • In packages/cellix/server-oauth2-mock-seedwork/src/index.ts, the README states CORS is locked down to local origins, but the implementation uses Access-Control-Allow-Origin: *; consider aligning the code with the documented restriction or updating the docs to match the actual behavior.
  • In apps/api/start-dev.mjs, the NODE_EXTRA_CA_CERTS path assumes a POSIX-style HOME directory and a fixed ~/.portless/ca.pem location; you may want to defensively handle missing HOME or non-Unix environments and surface a clearer error if the CA file is absent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `packages/cellix/server-oauth2-mock-seedwork/src/index.ts`, the README states CORS is locked down to local origins, but the implementation uses `Access-Control-Allow-Origin: *`; consider aligning the code with the documented restriction or updating the docs to match the actual behavior.
- In `apps/api/start-dev.mjs`, the `NODE_EXTRA_CA_CERTS` path assumes a POSIX-style `HOME` directory and a fixed `~/.portless/ca.pem` location; you may want to defensively handle missing `HOME` or non-Unix environments and surface a clearer error if the CA file is absent.

## Individual Comments

### Comment 1
<location path="packages/cellix/ui-core/src/components/molecules/require-auth/index.tsx" line_range="63-67" />
<code_context>
+			</Row>
+		);
+	}
+	if (props.forceLogin === false) {
 		return <Navigate to="/" />;
-	} else {
-		redirectUser();
-		return;
 	}
+
+	return (
+		<Row
+			justify={'center'}
</code_context>
<issue_to_address>
**issue (bug_risk):** Unauthenticated + forceLogin undefined now leads to a spinner but never triggers sign-in

With this refactor, `signinRedirect()` is only called in the `useEffect` when `props.forceLogin === true`. When `forceLogin` is `false`, you correctly navigate to `/`. But when `forceLogin` is `undefined` (the common case), an unauthenticated user hits the final `return` that renders the “Redirecting to sign in...” spinner without ever calling `signinRedirect()`, so no redirect occurs. You could either treat `undefined` as `true` in the effect condition (e.g. `props.forceLogin !== false`) or call `signinRedirect()` explicitly in the final unauthenticated branch when `forceLogin` isn’t `false`.
</issue_to_address>

### Comment 2
<location path="packages/cellix/server-oauth2-mock-seedwork/src/index.ts" line_range="154-163" />
<code_context>
-	});
-
-	// Token endpoint
-	app.post('/token', async (req, res) => {
-		// biome-ignore lint:useLiteralKeys
-		const email = process.env['Email'];
</code_context>
<issue_to_address>
**suggestion:** Refresh token handling in /token is inconsistent with the `buildTokenResponse` API

In `/token` you reject `grant_type === 'refresh_token'` with `invalid_grant`, so refresh flows are effectively unsupported. Yet `buildTokenResponse` takes an `existingRefreshToken`, and `/token` passes `refresh_token` into it, even though that value is never meaningfully used. To avoid a misleading API, consider either implementing proper refresh token support (validate the refresh token and use it in `buildTokenResponse`) or removing the `existingRefreshToken` parameter and corresponding argument from `/token` so the behavior matches the intended capabilities of the mock server.
</issue_to_address>

### Comment 3
<location path="apps/api/start-dev.mjs" line_range="10-13" />
<code_context>
+	process.exit(1);
+}
+
+const childEnv = {
+	...process.env,
+	NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
+	NODE_OPTIONS: '--use-system-ca',
+};
+
</code_context>
<issue_to_address>
**suggestion:** Overwriting NODE_OPTIONS may clobber caller-provided Node flags

`childEnv` currently overwrites any existing `NODE_OPTIONS`, dropping flags like `--inspect` or memory limits that may be set by the caller. To preserve upstream configuration while still enabling system CAs, append instead of replace, e.g.

```ts
const childEnv = {
  ...process.env,
  NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
  NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(),
};
```
</issue_to_address>

### Comment 4
<location path="apps/server-oauth2-mock/package.json" line_range="12-13" />
<code_context>
+		"lint": "biome lint",
+		"format": "biome format --write",
+		"start": "node dist/index.js",
+		"dev": "portless mock-auth.ownercommunity.localhost node dist/index.js"
+	},
+	"dependencies": {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Dev script assumes a built dist, which can be surprising for local development

`dev` runs `node dist/index.js` via Portless but never ensures `dist` exists or is current. If someone runs `pnpm dev` before a build, they’ll likely get a runtime error. Consider either making `dev` run `build` first (e.g. `"dev": "pnpm run build && portless ..."`) or using a TS runner/watch build (e.g. `tsx`) to keep the dev loop smooth.

```suggestion
		"start": "node dist/index.js",
		"dev": "pnpm run build && portless mock-auth.ownercommunity.localhost node dist/index.js"
```
</issue_to_address>

### Comment 5
<location path="apps/server-mongodb-memory-mock/package.json" line_range="13" />
<code_context>
+		"lint": "biome lint",
+		"format": "biome format --write",
+		"start": "node dist/index.js",
+		"dev": "node dist/index.js"
+	},
+	"dependencies": {
</code_context>
<issue_to_address>
**suggestion:** Mongo memory mock dev script also relies on prebuilt JS without enforcing a build step

Because `dev` runs `node dist/index.js` without a preceding build or watch, it’s easy to run stale or missing output. Consider either chaining a build (or incremental build) before `node`, or running the TypeScript sources directly via a tool like `tsx` in watch mode to support iterative development.

Suggested implementation:

```
		"start": "node dist/index.js",
		"dev": "tsx watch src/index.ts"

```

```
		"@cellix/config-typescript": "workspace:*",
		"rimraf": "catalog:",
		"typescript": "catalog:",
		"tsx": "catalog:"

```

If the entrypoint is not `src/index.ts`, update the `dev` script path accordingly (e.g. `src/main.ts` or similar). Also ensure your workspace catalog defines a version for `tsx`; if not, replace `"catalog:"` with an explicit version (e.g. `"^4.19.0"`).
</issue_to_address>

### Comment 6
<location path="apps/server-oauth2-mock/README.md" line_range="3" />
<code_context>
+# Mock OAuth2 Server
+
+A lightweight OpenID Connect compliant mock OAuth2 server for local development and testing. This server simulates OAuth2/OIDC flows without requiring external authentication infrastructure.
+
+## Quick Start
</code_context>
<issue_to_address>
**nitpick (typo):** Consider hyphenating "OpenID Connect–compliant" for grammatical correctness.

Here, it should read “A lightweight OpenID Connect–compliant mock OAuth2 server…” so that “OpenID Connect” correctly modifies “compliant.”

```suggestion
A lightweight OpenID Connect–compliant mock OAuth2 server for local development and testing. This server simulates OAuth2/OIDC flows without requiring external authentication infrastructure.
```
</issue_to_address>

### Comment 7
<location path="apps/server-oauth2-mock/README.md" line_range="43" />
<code_context>
+| `FAMILY_NAME` | `User` | User family name in tokens |
+| `SUB` | *auto-generated* | User subject ID (optional - see below) |
+| `TID` | `test-tenant-id` | Tenant ID in tokens |
+| `PORT` | `4000` | Server port (dev mode only) |
+| `PORTLESS_URL` | *not set* | Portless proxy URL (set automatically in dev mode) |
+
</code_context>
<issue_to_address>
**issue:** Clarify when the `PORT` variable is used; "dev mode only" appears misleading.

The earlier "Production/Direct Mode" section states that `PORT` configures the server on `http://localhost:4000`. Labeling it as "dev mode only" here is inconsistent and may confuse readers. Please rename or adjust the description to clarify whether `PORT` applies to both dev and production modes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +63 to +67
if (props.forceLogin === false) {
return <Navigate to="/" />;
} else {
redirectUser();
return;
}

return (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Unauthenticated + forceLogin undefined now leads to a spinner but never triggers sign-in

With this refactor, signinRedirect() is only called in the useEffect when props.forceLogin === true. When forceLogin is false, you correctly navigate to /. But when forceLogin is undefined (the common case), an unauthenticated user hits the final return that renders the “Redirecting to sign in...” spinner without ever calling signinRedirect(), so no redirect occurs. You could either treat undefined as true in the effect condition (e.g. props.forceLogin !== false) or call signinRedirect() explicitly in the final unauthenticated branch when forceLogin isn’t false.

Comment on lines +154 to +163
app.post('/token', async (req, res) => {
const { grant_type, refresh_token, tid, code } = req.body as {
grant_type?: string;
refresh_token?: string;
tid?: string;
code?: string;
};

if (grant_type === 'refresh_token') {
res.status(400).json({ error: 'invalid_grant' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Refresh token handling in /token is inconsistent with the buildTokenResponse API

In /token you reject grant_type === 'refresh_token' with invalid_grant, so refresh flows are effectively unsupported. Yet buildTokenResponse takes an existingRefreshToken, and /token passes refresh_token into it, even though that value is never meaningfully used. To avoid a misleading API, consider either implementing proper refresh token support (validate the refresh token and use it in buildTokenResponse) or removing the existingRefreshToken parameter and corresponding argument from /token so the behavior matches the intended capabilities of the mock server.

Comment on lines +10 to +13
const childEnv = {
...process.env,
NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
NODE_OPTIONS: '--use-system-ca',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Overwriting NODE_OPTIONS may clobber caller-provided Node flags

childEnv currently overwrites any existing NODE_OPTIONS, dropping flags like --inspect or memory limits that may be set by the caller. To preserve upstream configuration while still enabling system CAs, append instead of replace, e.g.

const childEnv = {
  ...process.env,
  NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
  NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(),
};

Comment on lines +12 to +13
"start": "node dist/index.js",
"dev": "portless mock-auth.ownercommunity.localhost node dist/index.js"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Dev script assumes a built dist, which can be surprising for local development

dev runs node dist/index.js via Portless but never ensures dist exists or is current. If someone runs pnpm dev before a build, they’ll likely get a runtime error. Consider either making dev run build first (e.g. "dev": "pnpm run build && portless ...") or using a TS runner/watch build (e.g. tsx) to keep the dev loop smooth.

Suggested change
"start": "node dist/index.js",
"dev": "portless mock-auth.ownercommunity.localhost node dist/index.js"
"start": "node dist/index.js",
"dev": "pnpm run build && portless mock-auth.ownercommunity.localhost node dist/index.js"

"lint": "biome lint",
"format": "biome format --write",
"start": "node dist/index.js",
"dev": "node dist/index.js"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Mongo memory mock dev script also relies on prebuilt JS without enforcing a build step

Because dev runs node dist/index.js without a preceding build or watch, it’s easy to run stale or missing output. Consider either chaining a build (or incremental build) before node, or running the TypeScript sources directly via a tool like tsx in watch mode to support iterative development.

Suggested implementation:

		"start": "node dist/index.js",
		"dev": "tsx watch src/index.ts"

		"@cellix/config-typescript": "workspace:*",
		"rimraf": "catalog:",
		"typescript": "catalog:",
		"tsx": "catalog:"

If the entrypoint is not src/index.ts, update the dev script path accordingly (e.g. src/main.ts or similar). Also ensure your workspace catalog defines a version for tsx; if not, replace "catalog:" with an explicit version (e.g. "^4.19.0").

@@ -0,0 +1,191 @@
# Mock OAuth2 Server

A lightweight OpenID Connect compliant mock OAuth2 server for local development and testing. This server simulates OAuth2/OIDC flows without requiring external authentication infrastructure.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (typo): Consider hyphenating "OpenID Connect–compliant" for grammatical correctness.

Here, it should read “A lightweight OpenID Connect–compliant mock OAuth2 server…” so that “OpenID Connect” correctly modifies “compliant.”

Suggested change
A lightweight OpenID Connect compliant mock OAuth2 server for local development and testing. This server simulates OAuth2/OIDC flows without requiring external authentication infrastructure.
A lightweight OpenID Connectcompliant mock OAuth2 server for local development and testing. This server simulates OAuth2/OIDC flows without requiring external authentication infrastructure.

| `FAMILY_NAME` | `User` | User family name in tokens |
| `SUB` | *auto-generated* | User subject ID (optional - see below) |
| `TID` | `test-tenant-id` | Tenant ID in tokens |
| `PORT` | `4000` | Server port (dev mode only) |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: Clarify when the PORT variable is used; "dev mode only" appears misleading.

The earlier "Production/Direct Mode" section states that PORT configures the server on http://localhost:4000. Labeling it as "dev mode only" here is inconsistent and may confuse readers. Please rename or adjust the description to clarify whether PORT applies to both dev and production modes.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In start-dev.mjs for the API, the NODE_EXTRA_CA_CERTS path is built from process.env.HOME, which may not exist or behave as expected on non-Unix platforms; consider resolving the CA file using os.homedir() or a more robust cross-platform approach.
  • The OAuth2 mock seedwork README claims CORS is "locked down to local origins", but the implementation sets Access-Control-Allow-Origin to *; either tighten the CORS logic to match the description or update the docs to reflect the current behavior.
  • In the mock OAuth2 token endpoint, grant_type === 'refresh_token' is explicitly rejected even though buildTokenResponse carries through an existing refresh_token; if refresh tokens are intentionally unsupported, consider making that explicit in the config/README or remove the unused parameter handling to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `start-dev.mjs` for the API, the `NODE_EXTRA_CA_CERTS` path is built from `process.env.HOME`, which may not exist or behave as expected on non-Unix platforms; consider resolving the CA file using `os.homedir()` or a more robust cross-platform approach.
- The OAuth2 mock seedwork README claims CORS is "locked down to local origins", but the implementation sets `Access-Control-Allow-Origin` to `*`; either tighten the CORS logic to match the description or update the docs to reflect the current behavior.
- In the mock OAuth2 token endpoint, `grant_type === 'refresh_token'` is explicitly rejected even though `buildTokenResponse` carries through an existing `refresh_token`; if refresh tokens are intentionally unsupported, consider making that explicit in the config/README or remove the unused parameter handling to avoid confusion.

## Individual Comments

### Comment 1
<location path="packages/cellix/ui-core/src/components/molecules/require-auth/index.tsx" line_range="63-68" />
<code_context>
+			</Row>
+		);
+	}
+	if (props.forceLogin === false) {
 		return <Navigate to="/" />;
-	} else {
-		redirectUser();
-		return;
 	}
+
+	return (
+		<Row
+			justify={'center'}
+			style={{ height: '100vh', alignItems: 'center' }}
</code_context>
<issue_to_address>
**issue (bug_risk):** Non-`forceLogin` flows no longer trigger a redirect, leaving users stuck on the spinner.

Previously, when `auth.isAuthenticated` was false and there was no error, we always ended up calling `redirectUser()` for non-`forceLogin` flows. With this change:

- The auto-redirect `useEffect` now only runs when `props.forceLogin === true`.
- The final `return` only renders the "Redirecting to sign in..." spinner and never calls `signinRedirect`.
- For `props.forceLogin === undefined` (or any non-`false` value other than `true`), users will see the spinner but never be redirected.

If you want to preserve the previous default behavior, we should either:
- Call `auth.signinRedirect()` when `forceLogin` is not explicitly `false`, or
- Relax the `useEffect` condition to treat `forceLogin !== false` as eligible for auto sign-in.

Otherwise, all call sites should be updated to pass an explicit `forceLogin` to avoid flows that hang on the spinner.
</issue_to_address>

### Comment 2
<location path="packages/cellix/server-oauth2-mock-seedwork/src/index.ts" line_range="223-225" />
<code_context>
+
+	app.get('/authorize', (req, res) => {
+		const { state, redirect_uri } = req.query as { state?: string; redirect_uri?: string };
+		const requestedRedirectUri = redirect_uri ?? config.allowedRedirectUri;
+		const normalizedRequestedRedirectUri = normalizeUrl(requestedRedirectUri);
+		const isAllowed = [...config.allowedRedirectUris].some((value) => normalizeUrl(value) === normalizedRequestedRedirectUri) || normalizeUrl(config.allowedRedirectUri) === normalizedRequestedRedirectUri;
+		if (!isAllowed) {
+			res.status(400).send('Invalid redirect_uri');
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Redirect URI normalization is used for validation but not for later audience lookup, which can cause subtle mismatches.

The `/authorize` handler normalizes the redirect URI for validation, but the `/token` handler later decodes and uses the raw `redirect_uri` when looking up `config.allowedRedirectUris` / `redirectUriToAudience`. A URI that passes validation via normalization may then miss in `redirectUriToAudience` and fall back to the default audience. To keep `aud` consistent, consider normalizing both the stored keys in `allowedRedirectUris` / `redirectUriToAudience` and the decoded redirect URI before lookup.

Suggested implementation:

```typescript
		const requestedRedirectUri = redirect_uri ?? config.allowedRedirectUri;
		const normalizedRequestedRedirectUri = normalizeUrl(requestedRedirectUri);
		const isAllowed =
			[...config.allowedRedirectUris].some(
				(value) => normalizeUrl(value) === normalizedRequestedRedirectUri,
			) || normalizeUrl(config.allowedRedirectUri) === normalizedRequestedRedirectUri;

		if (!isAllowed) {
			res.status(400).send('Invalid redirect_uri');
			return;
		}

		try {
			// Use the normalized redirect URI when embedding into the auth code so that
			// the `/token` handler can later decode and perform audience lookup on a
			// normalized value consistent with validation.
			const code = `mock-auth-code-${Buffer.from(normalizedRequestedRedirectUri).toString('base64')}`;
			const redirectUrl = new URL(requestedRedirectUri);

```

To fully implement your suggestion and avoid subtle mismatches between `/authorize` and `/token`, you will also need to:

1. Normalize keys in configuration:
   - Wherever `config.allowedRedirectUris` and `config.redirectUriToAudience` are constructed (or immediately after loading config), create normalized variants, for example:
     - `normalizedAllowedRedirectUris = new Set([...config.allowedRedirectUris].map(normalizeUrl))`
     - `normalizedRedirectUriToAudience = new Map([...config.redirectUriToAudience].map(([uri, aud]) => [normalizeUrl(uri), aud]))`
   - Use these normalized collections for validation and audience lookup instead of the raw ones.

2. Normalize the decoded redirect URI in the `/token` handler:
   - In the `/token` route, after decoding the redirect URI from the auth code (or reading from `req.body.redirect_uri`), call `normalizeUrl(decodedRedirectUri)` and use that normalized value when:
     - Checking membership against `normalizedAllowedRedirectUris`
     - Looking up the audience from `normalizedRedirectUriToAudience`

3. Ensure that `/authorize` and `/token` use the same normalization function:
   - Confirm both handlers import and use the same `normalizeUrl` implementation so that the normalization is consistent across validation and lookup.
</issue_to_address>

### Comment 3
<location path="apps/api/start-dev.mjs" line_range="10-13" />
<code_context>
+	process.exit(1);
+}
+
+const childEnv = {
+	...process.env,
+	NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
+	NODE_OPTIONS: '--use-system-ca',
+};
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Deriving the Portless CA path from `HOME` may not be robust across environments.

This builds the CA path as `${process.env.HOME}/.portless/ca.pem`. In some environments (e.g., certain Windows setups or CI runners), `HOME` may be unset or the directory may live under a different base var (like `USERPROFILE`), which would break `NODE_EXTRA_CA_CERTS` and cause TLS failures.

Consider using `os.homedir()` instead of `process.env.HOME`, and/or supporting an override env such as `PORTLESS_CA_PATH` with the current path as a fallback.

Suggested implementation:

```javascript
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';

```

```javascript
const portlessCaPath =
	process.env.PORTLESS_CA_PATH ??
	path.join(os.homedir(), '.portless', 'ca.pem');

const childEnv = {
	...process.env,
	NODE_EXTRA_CA_CERTS: portlessCaPath,

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +63 to +68
if (props.forceLogin === false) {
return <Navigate to="/" />;
} else {
redirectUser();
return;
}

return (
<Row
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Non-forceLogin flows no longer trigger a redirect, leaving users stuck on the spinner.

Previously, when auth.isAuthenticated was false and there was no error, we always ended up calling redirectUser() for non-forceLogin flows. With this change:

  • The auto-redirect useEffect now only runs when props.forceLogin === true.
  • The final return only renders the "Redirecting to sign in..." spinner and never calls signinRedirect.
  • For props.forceLogin === undefined (or any non-false value other than true), users will see the spinner but never be redirected.

If you want to preserve the previous default behavior, we should either:

  • Call auth.signinRedirect() when forceLogin is not explicitly false, or
  • Relax the useEffect condition to treat forceLogin !== false as eligible for auto sign-in.

Otherwise, all call sites should be updated to pass an explicit forceLogin to avoid flows that hang on the spinner.

Comment on lines +223 to +225
const requestedRedirectUri = redirect_uri ?? config.allowedRedirectUri;
const normalizedRequestedRedirectUri = normalizeUrl(requestedRedirectUri);
const isAllowed = [...config.allowedRedirectUris].some((value) => normalizeUrl(value) === normalizedRequestedRedirectUri) || normalizeUrl(config.allowedRedirectUri) === normalizedRequestedRedirectUri;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Redirect URI normalization is used for validation but not for later audience lookup, which can cause subtle mismatches.

The /authorize handler normalizes the redirect URI for validation, but the /token handler later decodes and uses the raw redirect_uri when looking up config.allowedRedirectUris / redirectUriToAudience. A URI that passes validation via normalization may then miss in redirectUriToAudience and fall back to the default audience. To keep aud consistent, consider normalizing both the stored keys in allowedRedirectUris / redirectUriToAudience and the decoded redirect URI before lookup.

Suggested implementation:

		const requestedRedirectUri = redirect_uri ?? config.allowedRedirectUri;
		const normalizedRequestedRedirectUri = normalizeUrl(requestedRedirectUri);
		const isAllowed =
			[...config.allowedRedirectUris].some(
				(value) => normalizeUrl(value) === normalizedRequestedRedirectUri,
			) || normalizeUrl(config.allowedRedirectUri) === normalizedRequestedRedirectUri;

		if (!isAllowed) {
			res.status(400).send('Invalid redirect_uri');
			return;
		}

		try {
			// Use the normalized redirect URI when embedding into the auth code so that
			// the `/token` handler can later decode and perform audience lookup on a
			// normalized value consistent with validation.
			const code = `mock-auth-code-${Buffer.from(normalizedRequestedRedirectUri).toString('base64')}`;
			const redirectUrl = new URL(requestedRedirectUri);

To fully implement your suggestion and avoid subtle mismatches between /authorize and /token, you will also need to:

  1. Normalize keys in configuration:

    • Wherever config.allowedRedirectUris and config.redirectUriToAudience are constructed (or immediately after loading config), create normalized variants, for example:
      • normalizedAllowedRedirectUris = new Set([...config.allowedRedirectUris].map(normalizeUrl))
      • normalizedRedirectUriToAudience = new Map([...config.redirectUriToAudience].map(([uri, aud]) => [normalizeUrl(uri), aud]))
    • Use these normalized collections for validation and audience lookup instead of the raw ones.
  2. Normalize the decoded redirect URI in the /token handler:

    • In the /token route, after decoding the redirect URI from the auth code (or reading from req.body.redirect_uri), call normalizeUrl(decodedRedirectUri) and use that normalized value when:
      • Checking membership against normalizedAllowedRedirectUris
      • Looking up the audience from normalizedRedirectUriToAudience
  3. Ensure that /authorize and /token use the same normalization function:

    • Confirm both handlers import and use the same normalizeUrl implementation so that the normalization is consistent across validation and lookup.

Comment on lines +10 to +13
const childEnv = {
...process.env,
NODE_EXTRA_CA_CERTS: `${process.env.HOME}/.portless/ca.pem`,
NODE_OPTIONS: '--use-system-ca',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Deriving the Portless CA path from HOME may not be robust across environments.

This builds the CA path as ${process.env.HOME}/.portless/ca.pem. In some environments (e.g., certain Windows setups or CI runners), HOME may be unset or the directory may live under a different base var (like USERPROFILE), which would break NODE_EXTRA_CA_CERTS and cause TLS failures.

Consider using os.homedir() instead of process.env.HOME, and/or supporting an override env such as PORTLESS_CA_PATH with the current path as a fallback.

Suggested implementation:

import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
const portlessCaPath =
	process.env.PORTLESS_CA_PATH ??
	path.join(os.homedir(), '.portless', 'ca.pem');

const childEnv = {
	...process.env,
	NODE_EXTRA_CA_CERTS: portlessCaPath,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants