crossly.client.auth.service 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.vscode/launch.json +95 -0
  2. package/.vscode/settings.json +24 -0
  3. package/.vscode/tasks.json +34 -0
  4. package/README.md +38 -0
  5. package/contracts/README.md +28 -0
  6. package/contracts/package-lock.json +30 -0
  7. package/contracts/package.json +46 -0
  8. package/contracts/src/index.ts +53 -0
  9. package/contracts/tsconfig.json +22 -0
  10. package/dist/app.js +52 -0
  11. package/dist/app.js.map +1 -0
  12. package/dist/config.js +29 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/controllers/authController.js +213 -0
  15. package/dist/controllers/authController.js.map +1 -0
  16. package/dist/createApp.js +29 -0
  17. package/dist/createApp.js.map +1 -0
  18. package/dist/db/migrate.js +96 -0
  19. package/dist/db/migrate.js.map +1 -0
  20. package/dist/managers/authManager.js +79 -0
  21. package/dist/managers/authManager.js.map +1 -0
  22. package/dist/managers/types.js +2 -0
  23. package/dist/managers/types.js.map +1 -0
  24. package/dist/oidc/googleProvider.js +54 -0
  25. package/dist/oidc/googleProvider.js.map +1 -0
  26. package/dist/oidc/notConfiguredOidcProvider.js +14 -0
  27. package/dist/oidc/notConfiguredOidcProvider.js.map +1 -0
  28. package/dist/oidc/types.js +2 -0
  29. package/dist/oidc/types.js.map +1 -0
  30. package/dist/repository/inMemoryClientRepository.js +60 -0
  31. package/dist/repository/inMemoryClientRepository.js.map +1 -0
  32. package/dist/repository/pgClientRepository.js +45 -0
  33. package/dist/repository/pgClientRepository.js.map +1 -0
  34. package/dist/repository/types.js +2 -0
  35. package/dist/repository/types.js.map +1 -0
  36. package/dist/signer/jwtSigner.js +36 -0
  37. package/dist/signer/jwtSigner.js.map +1 -0
  38. package/dist/signer/types.js +2 -0
  39. package/dist/signer/types.js.map +1 -0
  40. package/docker-compose.yml +25 -0
  41. package/migrations/0001_create_clients.sql +16 -0
  42. package/package.json +50 -0
  43. package/src/app.ts +61 -0
  44. package/src/config.ts +51 -0
  45. package/src/controllers/authController.ts +237 -0
  46. package/src/createApp.ts +45 -0
  47. package/src/db/migrate.ts +106 -0
  48. package/src/managers/authManager.ts +105 -0
  49. package/src/managers/types.ts +59 -0
  50. package/src/oidc/googleProvider.ts +72 -0
  51. package/src/oidc/notConfiguredOidcProvider.ts +16 -0
  52. package/src/oidc/types.ts +41 -0
  53. package/src/repository/inMemoryClientRepository.ts +72 -0
  54. package/src/repository/pgClientRepository.ts +75 -0
  55. package/src/repository/types.ts +50 -0
  56. package/src/signer/jwtSigner.ts +49 -0
  57. package/src/signer/types.ts +14 -0
  58. package/tests/integration/auth.api.test.ts +212 -0
  59. package/tests/integration/fakeOidcProvider.ts +32 -0
  60. package/tests/unit/authManager.test.ts +87 -0
  61. package/tests/unit/clientRepository.test.ts +115 -0
  62. package/tests/unit/jwtSigner.test.ts +42 -0
  63. package/tests/unit/resolveLogin.test.ts +86 -0
  64. package/tsconfig.build.json +11 -0
  65. package/tsconfig.json +24 -0
  66. package/tsconfig.test.json +25 -0
@@ -0,0 +1,95 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "type": "node",
6
+ "request": "launch",
7
+ "name": "Debug Current Test File",
8
+ "program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
9
+ "runtimeArgs": [
10
+ "--enable-source-maps"
11
+ ],
12
+ "args": [
13
+ "--timeout",
14
+ "30000",
15
+ "${workspaceFolder}/dist-tests/**/${fileBasenameNoExtension}.js"
16
+ ],
17
+ "console": "integratedTerminal",
18
+ "internalConsoleOptions": "neverOpen",
19
+ "skipFiles": [
20
+ "<node_internals>/**"
21
+ ],
22
+ "outFiles": [
23
+ "${workspaceFolder}/dist-tests/**/*.js"
24
+ ],
25
+ "sourceMaps": true,
26
+ "sourceMapPathOverrides": {
27
+ "../src/*": "${workspaceFolder}/src/*",
28
+ "../tests/*": "${workspaceFolder}/tests/*"
29
+ },
30
+ "resolveSourceMapLocations": [
31
+ "${workspaceFolder}/**",
32
+ "!**/node_modules/**"
33
+ ],
34
+ "preLaunchTask": "compile:tests"
35
+ },
36
+ {
37
+ "type": "node",
38
+ "request": "launch",
39
+ "name": "Debug All Tests",
40
+ "program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
41
+ "runtimeArgs": [
42
+ "--enable-source-maps"
43
+ ],
44
+ "args": [
45
+ "--timeout",
46
+ "30000",
47
+ "${workspaceFolder}/dist-tests/tests/**/*.test.js"
48
+ ],
49
+ "console": "integratedTerminal",
50
+ "internalConsoleOptions": "neverOpen",
51
+ "skipFiles": [
52
+ "<node_internals>/**"
53
+ ],
54
+ "outFiles": [
55
+ "${workspaceFolder}/dist-tests/**/*.js"
56
+ ],
57
+ "sourceMaps": true,
58
+ "sourceMapPathOverrides": {
59
+ "../src/*": "${workspaceFolder}/src/*",
60
+ "../tests/*": "${workspaceFolder}/tests/*"
61
+ },
62
+ "resolveSourceMapLocations": [
63
+ "${workspaceFolder}/**",
64
+ "!**/node_modules/**"
65
+ ],
66
+ "preLaunchTask": "build:tests"
67
+ },
68
+ {
69
+ "type": "node",
70
+ "request": "launch",
71
+ "name": "Debug App",
72
+ "program": "${workspaceFolder}/dist/app.js",
73
+ "runtimeArgs": [
74
+ "--enable-source-maps"
75
+ ],
76
+ "console": "integratedTerminal",
77
+ "internalConsoleOptions": "neverOpen",
78
+ "skipFiles": [
79
+ "<node_internals>/**"
80
+ ],
81
+ "outFiles": [
82
+ "${workspaceFolder}/dist/**/*.js"
83
+ ],
84
+ "sourceMaps": true,
85
+ "sourceMapPathOverrides": {
86
+ "../src/*": "${workspaceFolder}/src/*"
87
+ },
88
+ "resolveSourceMapLocations": [
89
+ "${workspaceFolder}/**",
90
+ "!**/node_modules/**"
91
+ ],
92
+ "preLaunchTask": "build"
93
+ }
94
+ ]
95
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "cSpell.words": [
3
+ "textyly"
4
+ ],
5
+ "files.exclude": {
6
+ "**/dist*": true,
7
+ "**/package*.json": false,
8
+ "**/node_modules": true,
9
+ "**/tsconfig*.json": true,
10
+ "**/.gitignore": true,
11
+ "**/.github": true,
12
+ "**/LICENSE": true,
13
+ "**/README*": true,
14
+ "**/*.js": true,
15
+ "**/*.js.map": true,
16
+ "**/*.log": true
17
+ },
18
+ "typescript.preferences.importModuleSpecifierEnding": "js",
19
+ "typescript.tsdk": "node_modules/typescript/lib",
20
+ "typescript.enablePromptUseWorkspaceTsdk": true,
21
+ "editor.formatOnSave": true,
22
+ "files.trimTrailingWhitespace": true,
23
+ "js/ts.tsdk.path": "node_modules/typescript/lib"
24
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "build",
6
+ "type": "shell",
7
+ "command": "npm run build",
8
+ "group": {
9
+ "kind": "build",
10
+ "isDefault": true
11
+ },
12
+ "problemMatcher": [],
13
+ "detail": "Run the build script using npm."
14
+ },
15
+ {
16
+ "label": "build:tests",
17
+ "type": "shell",
18
+ "command": "npm run build:tests",
19
+ "group": "build",
20
+ "problemMatcher": [],
21
+ "detail": "Full build then compile the tests using npm."
22
+ },
23
+ {
24
+ "label": "compile:tests",
25
+ "type": "shell",
26
+ "command": "npm run compile:tests",
27
+ "group": "build",
28
+ "problemMatcher": [
29
+ "$tsc"
30
+ ],
31
+ "detail": "Fast: recompile the tests (and imported source) for debugging."
32
+ }
33
+ ]
34
+ }
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # crossly.client.auth.service
2
+
3
+ Client authentication for Crossly.
4
+
5
+ Phase 1 (this version) is deliberately minimal: it issues **anonymous guest sessions** so a user can
6
+ start creating patterns immediately, with no login. A guest session is a signed JWT whose `sub` is a
7
+ freshly generated client id.
8
+
9
+ Sessions are valid for **1 year**, and `POST /auth/refresh` re-issues a token for the same client id
10
+ with a fresh expiry. The client calls it on use (e.g. on app start) so an active user's session keeps
11
+ rolling forward and only lapses after a full year of inactivity.
12
+
13
+ Later phases add login via existing identity providers (Google, GitHub, …) that issue a token of the
14
+ **same shape**, so nothing downstream changes — see the project notes for the staged plan.
15
+
16
+ ## Endpoints
17
+
18
+ | Method | Route | Purpose |
19
+ |--------|------------------|---------------------------------------------------------------|
20
+ | GET | `/health` | Liveness check |
21
+ | POST | `/auth/guest` | Create a new anonymous guest session |
22
+ | POST | `/auth/refresh` | Re-issue a token for the current session (`Authorization: Bearer <token>`) |
23
+ | GET | `/auth/validate` | Verify a token; on success returns `200` with `X-Client-Id` / `X-Guest` response headers, else `401`. Intended for an API-gateway **ForwardAuth** check so downstream services receive a trusted `clientId` without holding the signing key. |
24
+
25
+ ## Scripts
26
+
27
+ - `npm run build` — build contracts then the service
28
+ - `npm start` — build and run on port 5001
29
+ - `npm test` — unit + integration tests
30
+ - `npm run test:unit` / `npm run test:integration`
31
+
32
+ ## Configuration
33
+
34
+ - `AUTH_JWT_SECRET` — HS256 signing secret (defaults to a dev-only value; override in real environments).
35
+
36
+ ## License
37
+
38
+ Apache-2.0
@@ -0,0 +1,28 @@
1
+ # @textyly/crossly-client-auth-contracts
2
+
3
+ Shared TypeScript contracts (types / DTOs) for the Crossly **Client Auth** service.
4
+
5
+ These types describe the auth tokens and responses exchanged over the service's HTTP API and are
6
+ meant to be consumed by any TypeScript/JavaScript client — for example [`crossly.ui`](https://github.com/textyly) —
7
+ and by other services that validate Crossly access tokens.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @textyly/crossly-client-auth-contracts
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import type {
19
+ AccessTokenClaims,
20
+ GuestSessionResponse,
21
+ } from '@textyly/crossly-client-auth-contracts';
22
+ ```
23
+
24
+ The package ships only type declarations and has no runtime or server dependencies.
25
+
26
+ ## License
27
+
28
+ Apache-2.0 — part of the [textyly / crossly](https://github.com/textyly) open-source project.
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@textyly/crossly-client-auth-contracts",
3
+ "version": "0.0.1",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@textyly/crossly-client-auth-contracts",
9
+ "version": "0.0.1",
10
+ "license": "Apache-2.0",
11
+ "devDependencies": {
12
+ "typescript": "^5.6.2"
13
+ }
14
+ },
15
+ "node_modules/typescript": {
16
+ "version": "5.9.3",
17
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
18
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
19
+ "dev": true,
20
+ "license": "Apache-2.0",
21
+ "bin": {
22
+ "tsc": "bin/tsc",
23
+ "tsserver": "bin/tsserver"
24
+ },
25
+ "engines": {
26
+ "node": ">=14.17"
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@textyly/crossly-client-auth-contracts",
3
+ "version": "0.0.2",
4
+ "description": "Shared contracts (types/DTOs) for the Crossly Client Auth service, consumable by clients such as crossly.ui.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "compile": "tsc --version && tsc",
22
+ "build": "npm i && npm run compile",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/textyly/crossly.client.auth.service.git",
28
+ "directory": "contracts"
29
+ },
30
+ "homepage": "https://github.com/textyly/crossly.client.auth.service/tree/main/contracts#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/textyly/crossly.client.auth.service/issues"
33
+ },
34
+ "keywords": [
35
+ "crossly",
36
+ "textyly",
37
+ "contracts",
38
+ "auth",
39
+ "jwt"
40
+ ],
41
+ "author": "textyly community",
42
+ "license": "Apache-2.0",
43
+ "devDependencies": {
44
+ "typescript": "^5.6.2"
45
+ }
46
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared contracts for the Crossly Client Auth service.
3
+ *
4
+ * These types describe the auth tokens and responses exchanged over the
5
+ * service's API and are meant to be consumed by any TypeScript/JavaScript
6
+ * client (e.g. crossly.ui) and by other services that validate tokens.
7
+ * Keep this module free of runtime/server dependencies.
8
+ */
9
+
10
+ /** Claims carried by a Crossly access token (guest now; authenticated later). */
11
+ export interface AccessTokenClaims {
12
+ /** Subject — the stable client identifier. Equals the clientId used elsewhere. */
13
+ sub: string;
14
+ /** True for anonymous guest sessions; false for authenticated users. */
15
+ guest: boolean;
16
+ /** Issued-at, in seconds since the epoch. */
17
+ iat: number;
18
+ /** Expiry, in seconds since the epoch. */
19
+ exp: number;
20
+ }
21
+
22
+ /**
23
+ * Internal session shape produced by the signer/manager: the raw token + expiry.
24
+ * In the BFF cookie model the token is set as an httpOnly cookie rather than
25
+ * returned in the response body — see {@link SessionResponse} / {@link MeResponse}.
26
+ */
27
+ export interface GuestSessionResponse {
28
+ /** Signed JWT (set by the server as an httpOnly session cookie). */
29
+ token: string;
30
+ /** The client identifier (equals the token's `sub`). */
31
+ clientId: string;
32
+ /** Token expiry, in seconds since the epoch. */
33
+ expiresAt: number;
34
+ }
35
+
36
+ /**
37
+ * Minimal session summary returned by `POST /auth/guest`, `POST /auth/refresh`
38
+ * and `GET /auth/validate`. The session token itself rides in an httpOnly cookie.
39
+ */
40
+ export interface SessionResponse {
41
+ /** The client identifier (equals the session token's `sub`). */
42
+ clientId: string;
43
+ /** True for anonymous guests; false for authenticated users. */
44
+ guest: boolean;
45
+ }
46
+
47
+ /** Identity returned by `GET /auth/me` so the UI knows who it is (it can't read the cookie). */
48
+ export interface MeResponse {
49
+ clientId: string;
50
+ guest: boolean;
51
+ /** Present for authenticated users when the provider reported one; display only. */
52
+ email?: string;
53
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "strict": true,
11
+ "sourceMap": true,
12
+ "inlineSources": true,
13
+ "esModuleInterop": true
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "node_modules",
20
+ "dist"
21
+ ]
22
+ }
package/dist/app.js ADDED
@@ -0,0 +1,52 @@
1
+ import 'dotenv/config';
2
+ import pg from 'pg';
3
+ import { createApp } from './createApp.js';
4
+ import { JwtSigner } from './signer/jwtSigner.js';
5
+ import { PgClientRepository } from './repository/pgClientRepository.js';
6
+ import { databaseUrl, runMigrations } from './db/migrate.js';
7
+ import { loadConfig, loadGoogleConfig } from './config.js';
8
+ import { GoogleProvider } from './oidc/googleProvider.js';
9
+ import { NotConfiguredOidcProvider } from './oidc/notConfiguredOidcProvider.js';
10
+ const port = 5001;
11
+ // HS256 shared secret. MUST be overridden via AUTH_JWT_SECRET in any real environment.
12
+ const secret = process.env.AUTH_JWT_SECRET ?? 'dev-only-insecure-secret-change-me';
13
+ /** Build the real Google provider if configured, else a disabled placeholder. */
14
+ async function buildOidcProvider() {
15
+ const google = loadGoogleConfig();
16
+ if (google.clientId && google.clientSecret) {
17
+ return GoogleProvider.create({
18
+ clientId: google.clientId,
19
+ clientSecret: google.clientSecret,
20
+ redirectUri: google.callbackUrl,
21
+ });
22
+ }
23
+ console.warn('Google OIDC not configured (set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET) — /auth/login is disabled');
24
+ return new NotConfiguredOidcProvider();
25
+ }
26
+ /**
27
+ * Entry point: apply database migrations, then build the app via the shared
28
+ * factory and listen.
29
+ *
30
+ * Migrations run on startup and are safe across several instances booting in
31
+ * parallel (an advisory lock serializes them — see db/migrate.ts), so no separate
32
+ * migration job is required. The service will not start serving until the schema
33
+ * is up to date, which means a reachable Postgres is now required to boot.
34
+ */
35
+ async function main() {
36
+ await runMigrations();
37
+ // One pool for the app's lifetime, backing the client repository.
38
+ const pool = new pg.Pool({ connectionString: databaseUrl() });
39
+ const signer = new JwtSigner(secret);
40
+ const clients = new PgClientRepository(pool);
41
+ const oidc = await buildOidcProvider();
42
+ const config = loadConfig();
43
+ const app = createApp({ signer, clients, oidc, config });
44
+ app.listen(port, () => {
45
+ console.log(`crossly.client.auth.service listening on port ${port}`);
46
+ });
47
+ }
48
+ main().catch((error) => {
49
+ console.error('failed to start crossly.client.auth.service:', error);
50
+ process.exit(1);
51
+ });
52
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAGhF,MAAM,IAAI,GAAG,IAAI,CAAC;AAClB,uFAAuF;AACvF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,oCAAoC,CAAC;AAEnF,iFAAiF;AACjF,KAAK,UAAU,iBAAiB;IAC5B,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACzC,OAAO,cAAc,CAAC,MAAM,CAAC;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,WAAW,EAAE,MAAM,CAAC,WAAW;SAClC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,CAAC,IAAI,CACR,oGAAoG,CACvG,CAAC;IACF,OAAO,IAAI,yBAAyB,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,IAAI;IACf,MAAM,aAAa,EAAE,CAAC;IAEtB,kEAAkE;IAClE,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,iBAAiB,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAEzD,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QAClB,OAAO,CAAC,GAAG,CAAC,iDAAiD,IAAI,EAAE,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACP,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;IACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC","sourcesContent":["import 'dotenv/config';\nimport pg from 'pg';\nimport { createApp } from './createApp.js';\nimport { JwtSigner } from './signer/jwtSigner.js';\nimport { PgClientRepository } from './repository/pgClientRepository.js';\nimport { databaseUrl, runMigrations } from './db/migrate.js';\nimport { loadConfig, loadGoogleConfig } from './config.js';\nimport { GoogleProvider } from './oidc/googleProvider.js';\nimport { NotConfiguredOidcProvider } from './oidc/notConfiguredOidcProvider.js';\nimport type { IOidcProvider } from './oidc/types.js';\n\nconst port = 5001;\n// HS256 shared secret. MUST be overridden via AUTH_JWT_SECRET in any real environment.\nconst secret = process.env.AUTH_JWT_SECRET ?? 'dev-only-insecure-secret-change-me';\n\n/** Build the real Google provider if configured, else a disabled placeholder. */\nasync function buildOidcProvider(): Promise<IOidcProvider> {\n const google = loadGoogleConfig();\n if (google.clientId && google.clientSecret) {\n return GoogleProvider.create({\n clientId: google.clientId,\n clientSecret: google.clientSecret,\n redirectUri: google.callbackUrl,\n });\n }\n\n console.warn(\n 'Google OIDC not configured (set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET) — /auth/login is disabled',\n );\n return new NotConfiguredOidcProvider();\n}\n\n/**\n * Entry point: apply database migrations, then build the app via the shared\n * factory and listen.\n *\n * Migrations run on startup and are safe across several instances booting in\n * parallel (an advisory lock serializes them — see db/migrate.ts), so no separate\n * migration job is required. The service will not start serving until the schema\n * is up to date, which means a reachable Postgres is now required to boot.\n */\nasync function main(): Promise<void> {\n await runMigrations();\n\n // One pool for the app's lifetime, backing the client repository.\n const pool = new pg.Pool({ connectionString: databaseUrl() });\n const signer = new JwtSigner(secret);\n const clients = new PgClientRepository(pool);\n const oidc = await buildOidcProvider();\n const config = loadConfig();\n const app = createApp({ signer, clients, oidc, config });\n\n app.listen(port, () => {\n console.log(`crossly.client.auth.service listening on port ${port}`);\n });\n}\n\nmain().catch((error) => {\n console.error('failed to start crossly.client.auth.service:', error);\n process.exit(1);\n});\n"]}
package/dist/config.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Service configuration, read from the environment with dev-friendly defaults.
3
+ * Secrets/URLs come from env (k8s Secret/ConfigMap later); the defaults make
4
+ * local dev work with zero setup (except Google login, which needs real creds).
5
+ */
6
+ /** httpOnly cookie holding the session JWT (guest or authenticated). */
7
+ export const SESSION_COOKIE = 'crossly_session';
8
+ /** Short-lived signed cookie holding the OAuth `state` + PKCE `codeVerifier`. */
9
+ export const OAUTH_COOKIE = 'crossly_oauth';
10
+ export function loadConfig() {
11
+ // Where the browser lands after login. May include a path (e.g. the static
12
+ // UI entry); the CORS origin is derived from just its scheme+host+port, since
13
+ // the browser's Origin header never carries a path.
14
+ const uiRedirectUrl = process.env.UI_URL ?? 'http://localhost:5000/dist/index.html';
15
+ return {
16
+ cookieSecret: process.env.COOKIE_SECRET ?? 'dev-only-cookie-secret-change-me',
17
+ uiRedirectUrl,
18
+ corsOrigin: process.env.CORS_ORIGIN ?? new URL(uiRedirectUrl).origin,
19
+ secureCookies: (process.env.SECURE_COOKIES ?? 'false') === 'true',
20
+ };
21
+ }
22
+ export function loadGoogleConfig() {
23
+ return {
24
+ clientId: process.env.GOOGLE_CLIENT_ID,
25
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
26
+ callbackUrl: process.env.GOOGLE_CALLBACK_URL ?? 'http://localhost:5001/api/v1/auth/callback',
27
+ };
28
+ }
29
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,eAAe,CAAC;AAqB5C,MAAM,UAAU,UAAU;IACtB,2EAA2E;IAC3E,8EAA8E;IAC9E,oDAAoD;IACpD,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,uCAAuC,CAAC;IACpF,OAAO;QACH,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,kCAAkC;QAC7E,aAAa;QACb,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,MAAM;QACpE,aAAa,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,KAAK,MAAM;KACpE,CAAC;AACN,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC5B,OAAO;QACH,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB;QACtC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;QAC9C,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,4CAA4C;KAC/F,CAAC;AACN,CAAC","sourcesContent":["/**\n * Service configuration, read from the environment with dev-friendly defaults.\n * Secrets/URLs come from env (k8s Secret/ConfigMap later); the defaults make\n * local dev work with zero setup (except Google login, which needs real creds).\n */\n\n/** httpOnly cookie holding the session JWT (guest or authenticated). */\nexport const SESSION_COOKIE = 'crossly_session';\n\n/** Short-lived signed cookie holding the OAuth `state` + PKCE `codeVerifier`. */\nexport const OAUTH_COOKIE = 'crossly_oauth';\n\n/** Runtime config for cookies, CORS and post-login redirects. */\nexport interface AuthConfig {\n /** Secret used to sign the short-lived OAuth cookie. */\n cookieSecret: string;\n /** Where the browser is sent after a successful login / after logout. */\n uiRedirectUrl: string;\n /** Allowed browser origin for credentialed CORS requests. */\n corsOrigin: string;\n /** Whether cookies carry the `Secure` attribute (true behind HTTPS). */\n secureCookies: boolean;\n}\n\n/** Google OAuth client config; clientId/secret absent ⇒ login is disabled. */\nexport interface GoogleConfig {\n clientId?: string;\n clientSecret?: string;\n callbackUrl: string;\n}\n\nexport function loadConfig(): AuthConfig {\n // Where the browser lands after login. May include a path (e.g. the static\n // UI entry); the CORS origin is derived from just its scheme+host+port, since\n // the browser's Origin header never carries a path.\n const uiRedirectUrl = process.env.UI_URL ?? 'http://localhost:5000/dist/index.html';\n return {\n cookieSecret: process.env.COOKIE_SECRET ?? 'dev-only-cookie-secret-change-me',\n uiRedirectUrl,\n corsOrigin: process.env.CORS_ORIGIN ?? new URL(uiRedirectUrl).origin,\n secureCookies: (process.env.SECURE_COOKIES ?? 'false') === 'true',\n };\n}\n\nexport function loadGoogleConfig(): GoogleConfig {\n return {\n clientId: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n callbackUrl: process.env.GOOGLE_CALLBACK_URL ?? 'http://localhost:5001/api/v1/auth/callback',\n };\n}\n"]}