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.
- package/.vscode/launch.json +95 -0
- package/.vscode/settings.json +24 -0
- package/.vscode/tasks.json +34 -0
- package/README.md +38 -0
- package/contracts/README.md +28 -0
- package/contracts/package-lock.json +30 -0
- package/contracts/package.json +46 -0
- package/contracts/src/index.ts +53 -0
- package/contracts/tsconfig.json +22 -0
- package/dist/app.js +52 -0
- package/dist/app.js.map +1 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/controllers/authController.js +213 -0
- package/dist/controllers/authController.js.map +1 -0
- package/dist/createApp.js +29 -0
- package/dist/createApp.js.map +1 -0
- package/dist/db/migrate.js +96 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/managers/authManager.js +79 -0
- package/dist/managers/authManager.js.map +1 -0
- package/dist/managers/types.js +2 -0
- package/dist/managers/types.js.map +1 -0
- package/dist/oidc/googleProvider.js +54 -0
- package/dist/oidc/googleProvider.js.map +1 -0
- package/dist/oidc/notConfiguredOidcProvider.js +14 -0
- package/dist/oidc/notConfiguredOidcProvider.js.map +1 -0
- package/dist/oidc/types.js +2 -0
- package/dist/oidc/types.js.map +1 -0
- package/dist/repository/inMemoryClientRepository.js +60 -0
- package/dist/repository/inMemoryClientRepository.js.map +1 -0
- package/dist/repository/pgClientRepository.js +45 -0
- package/dist/repository/pgClientRepository.js.map +1 -0
- package/dist/repository/types.js +2 -0
- package/dist/repository/types.js.map +1 -0
- package/dist/signer/jwtSigner.js +36 -0
- package/dist/signer/jwtSigner.js.map +1 -0
- package/dist/signer/types.js +2 -0
- package/dist/signer/types.js.map +1 -0
- package/docker-compose.yml +25 -0
- package/migrations/0001_create_clients.sql +16 -0
- package/package.json +50 -0
- package/src/app.ts +61 -0
- package/src/config.ts +51 -0
- package/src/controllers/authController.ts +237 -0
- package/src/createApp.ts +45 -0
- package/src/db/migrate.ts +106 -0
- package/src/managers/authManager.ts +105 -0
- package/src/managers/types.ts +59 -0
- package/src/oidc/googleProvider.ts +72 -0
- package/src/oidc/notConfiguredOidcProvider.ts +16 -0
- package/src/oidc/types.ts +41 -0
- package/src/repository/inMemoryClientRepository.ts +72 -0
- package/src/repository/pgClientRepository.ts +75 -0
- package/src/repository/types.ts +50 -0
- package/src/signer/jwtSigner.ts +49 -0
- package/src/signer/types.ts +14 -0
- package/tests/integration/auth.api.test.ts +212 -0
- package/tests/integration/fakeOidcProvider.ts +32 -0
- package/tests/unit/authManager.test.ts +87 -0
- package/tests/unit/clientRepository.test.ts +115 -0
- package/tests/unit/jwtSigner.test.ts +42 -0
- package/tests/unit/resolveLogin.test.ts +86 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +24 -0
- 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
|
package/dist/app.js.map
ADDED
|
@@ -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"]}
|