palaryn 0.1.0 → 0.3.2
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/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { OAuthConfig, OAuthProvider, OAuthProfile } from '../types/user';
|
|
4
|
+
import {
|
|
5
|
+
UserStore,
|
|
6
|
+
OAuthAccountStore,
|
|
7
|
+
SessionStore,
|
|
8
|
+
WorkspaceStore,
|
|
9
|
+
WorkspaceMemberStore,
|
|
10
|
+
UserApiKeyStore,
|
|
11
|
+
} from '../storage/interfaces';
|
|
12
|
+
import { generateCodeVerifier } from './pkce';
|
|
13
|
+
import { generateNonce } from './pkce';
|
|
14
|
+
import {
|
|
15
|
+
buildGoogleAuthUrl, exchangeGoogleCode, getGoogleUserInfo,
|
|
16
|
+
buildGitHubAuthUrl, exchangeGitHubCode, getGitHubUserInfo,
|
|
17
|
+
} from './providers';
|
|
18
|
+
import {
|
|
19
|
+
generateSessionId, encryptState, decryptState, encryptToken,
|
|
20
|
+
SESSION_COOKIE_NAME, STATE_COOKIE_NAME,
|
|
21
|
+
} from './session';
|
|
22
|
+
import { hashPassword, verifyPassword } from './password';
|
|
23
|
+
|
|
24
|
+
export interface AuthRouteDeps {
|
|
25
|
+
config: OAuthConfig;
|
|
26
|
+
userStore: UserStore;
|
|
27
|
+
oauthAccountStore: OAuthAccountStore;
|
|
28
|
+
sessionStore: SessionStore;
|
|
29
|
+
workspaceStore: WorkspaceStore;
|
|
30
|
+
workspaceMemberStore: WorkspaceMemberStore;
|
|
31
|
+
userApiKeyStore?: UserApiKeyStore;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createAuthRouter(deps: AuthRouteDeps): Router {
|
|
35
|
+
const router = Router();
|
|
36
|
+
const { config, userStore, oauthAccountStore, sessionStore, workspaceStore, workspaceMemberStore, userApiKeyStore } = deps;
|
|
37
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// GET /auth/:provider/authorize — redirect to OAuth provider
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
router.get('/:provider/authorize', (req: Request, res: Response) => {
|
|
43
|
+
const provider = req.params.provider as OAuthProvider;
|
|
44
|
+
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
|
45
|
+
|
|
46
|
+
if (provider === 'google' && config.google) {
|
|
47
|
+
const redirectUri = config.google.redirect_uri || `${baseUrl}/auth/google/callback`;
|
|
48
|
+
const nonce = generateNonce();
|
|
49
|
+
const codeVerifier = generateCodeVerifier();
|
|
50
|
+
|
|
51
|
+
const state = encryptState({
|
|
52
|
+
provider: 'google',
|
|
53
|
+
code_verifier: codeVerifier,
|
|
54
|
+
nonce,
|
|
55
|
+
redirect_uri: redirectUri,
|
|
56
|
+
created_at: Date.now(),
|
|
57
|
+
}, config.session_secret);
|
|
58
|
+
|
|
59
|
+
const { url } = buildGoogleAuthUrl(config.google, redirectUri, state, codeVerifier);
|
|
60
|
+
|
|
61
|
+
res.cookie(STATE_COOKIE_NAME, state, {
|
|
62
|
+
httpOnly: true,
|
|
63
|
+
secure: isProduction,
|
|
64
|
+
sameSite: 'lax',
|
|
65
|
+
maxAge: 600_000, // 10 min
|
|
66
|
+
path: '/',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
res.redirect(url);
|
|
70
|
+
} else if (provider === 'github' && config.github) {
|
|
71
|
+
const redirectUri = config.github.redirect_uri || `${baseUrl}/auth/github/callback`;
|
|
72
|
+
const nonce = generateNonce();
|
|
73
|
+
|
|
74
|
+
const state = encryptState({
|
|
75
|
+
provider: 'github',
|
|
76
|
+
nonce,
|
|
77
|
+
redirect_uri: redirectUri,
|
|
78
|
+
created_at: Date.now(),
|
|
79
|
+
}, config.session_secret);
|
|
80
|
+
|
|
81
|
+
const url = buildGitHubAuthUrl(config.github, redirectUri, state);
|
|
82
|
+
|
|
83
|
+
res.cookie(STATE_COOKIE_NAME, state, {
|
|
84
|
+
httpOnly: true,
|
|
85
|
+
secure: isProduction,
|
|
86
|
+
sameSite: 'lax',
|
|
87
|
+
maxAge: 600_000,
|
|
88
|
+
path: '/',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
res.redirect(url);
|
|
92
|
+
} else {
|
|
93
|
+
res.status(400).json({ error: `Unsupported or unconfigured provider: ${provider}` });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// GET /auth/:provider/callback — handle OAuth callback
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
router.get('/:provider/callback', async (req: Request, res: Response) => {
|
|
101
|
+
const provider = req.params.provider as OAuthProvider;
|
|
102
|
+
const { code, state: stateParam, error } = req.query as Record<string, string>;
|
|
103
|
+
|
|
104
|
+
if (error) {
|
|
105
|
+
return res.redirect(`/login?error=${encodeURIComponent(error)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!code || !stateParam) {
|
|
109
|
+
return res.redirect('/login?error=missing_params');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate state cookie
|
|
113
|
+
const stateCookie = req.cookies?.[STATE_COOKIE_NAME];
|
|
114
|
+
if (!stateCookie || stateCookie !== stateParam) {
|
|
115
|
+
return res.redirect('/login?error=invalid_state');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clear state cookie
|
|
119
|
+
res.clearCookie(STATE_COOKIE_NAME, { path: '/' });
|
|
120
|
+
|
|
121
|
+
let flowState;
|
|
122
|
+
try {
|
|
123
|
+
flowState = decryptState(stateCookie, config.session_secret);
|
|
124
|
+
} catch {
|
|
125
|
+
return res.redirect('/login?error=invalid_state');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check state expiry (10 min max)
|
|
129
|
+
if (Date.now() - flowState.created_at > 600_000) {
|
|
130
|
+
return res.redirect('/login?error=state_expired');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
let profile: OAuthProfile;
|
|
135
|
+
let accessToken: string;
|
|
136
|
+
let refreshToken: string | undefined;
|
|
137
|
+
|
|
138
|
+
if (provider === 'google' && config.google) {
|
|
139
|
+
const tokens = await exchangeGoogleCode(
|
|
140
|
+
config.google,
|
|
141
|
+
flowState.redirect_uri,
|
|
142
|
+
code,
|
|
143
|
+
flowState.code_verifier || '',
|
|
144
|
+
);
|
|
145
|
+
accessToken = tokens.access_token;
|
|
146
|
+
refreshToken = tokens.refresh_token;
|
|
147
|
+
profile = await getGoogleUserInfo(tokens.access_token);
|
|
148
|
+
} else if (provider === 'github' && config.github) {
|
|
149
|
+
const tokens = await exchangeGitHubCode(config.github, code);
|
|
150
|
+
accessToken = tokens.access_token;
|
|
151
|
+
profile = await getGitHubUserInfo(tokens.access_token);
|
|
152
|
+
} else {
|
|
153
|
+
return res.redirect('/login?error=unsupported_provider');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find or create user + link OAuth account
|
|
157
|
+
const { user, isNewUser, needsLinking } = findOrCreateUser(profile, accessToken, refreshToken);
|
|
158
|
+
|
|
159
|
+
// If an existing account with the same email was found but OAuth is not
|
|
160
|
+
// linked, redirect to login with a message instead of auto-linking.
|
|
161
|
+
if (needsLinking) {
|
|
162
|
+
return res.redirect('/login?error=account_exists&message=' +
|
|
163
|
+
encodeURIComponent('An account with this email already exists. Please sign in with your password first, then link your OAuth provider from settings.'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create session
|
|
167
|
+
const sessionId = generateSessionId();
|
|
168
|
+
const now = new Date().toISOString();
|
|
169
|
+
|
|
170
|
+
// Find user's first workspace
|
|
171
|
+
const memberships = workspaceMemberStore.getByUser(user.id);
|
|
172
|
+
const workspaceId = memberships.length > 0 ? memberships[0].workspace_id : undefined;
|
|
173
|
+
|
|
174
|
+
sessionStore.create({
|
|
175
|
+
id: sessionId,
|
|
176
|
+
user_id: user.id,
|
|
177
|
+
workspace_id: workspaceId,
|
|
178
|
+
ip_address: req.ip || req.socket.remoteAddress,
|
|
179
|
+
user_agent: req.get('user-agent'),
|
|
180
|
+
expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
|
|
181
|
+
last_active_at: now,
|
|
182
|
+
created_at: now,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Set session cookie
|
|
186
|
+
res.cookie(SESSION_COOKIE_NAME, sessionId, {
|
|
187
|
+
httpOnly: true,
|
|
188
|
+
secure: isProduction,
|
|
189
|
+
sameSite: 'lax',
|
|
190
|
+
maxAge: config.session_ttl_seconds * 1000,
|
|
191
|
+
path: '/',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Redirect based on user state
|
|
195
|
+
if (isNewUser || !user.onboarding_completed) {
|
|
196
|
+
return res.redirect('/onboarding');
|
|
197
|
+
}
|
|
198
|
+
return res.redirect('/dashboard');
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(`[auth] OAuth callback error (${provider}):`, err);
|
|
201
|
+
return res.redirect('/login?error=auth_failed');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// POST /auth/register — create account with email + password
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
router.post('/register', async (req: Request, res: Response) => {
|
|
209
|
+
const { email, password, display_name } = req.body || {};
|
|
210
|
+
|
|
211
|
+
if (!email || !password) {
|
|
212
|
+
res.status(400).json({ error: 'email and password are required' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
217
|
+
if (!emailRegex.test(email)) {
|
|
218
|
+
res.status(400).json({ error: 'Invalid email format' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (password.length < 6) {
|
|
223
|
+
res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const existing = userStore.getByEmail(email);
|
|
228
|
+
if (existing) {
|
|
229
|
+
// Return identical response to prevent account enumeration
|
|
230
|
+
res.status(201).json({
|
|
231
|
+
user: { id: randomUUID(), email, display_name: display_name || email.split('@')[0] },
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
const userId = randomUUID();
|
|
238
|
+
const displayName = display_name || email.split('@')[0];
|
|
239
|
+
userStore.create({
|
|
240
|
+
id: userId,
|
|
241
|
+
email,
|
|
242
|
+
display_name: displayName,
|
|
243
|
+
password_hash: await hashPassword(password),
|
|
244
|
+
status: 'active',
|
|
245
|
+
onboarding_completed: false,
|
|
246
|
+
created_at: now,
|
|
247
|
+
updated_at: now,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Auto-login: create session
|
|
251
|
+
const sessionId = generateSessionId();
|
|
252
|
+
sessionStore.create({
|
|
253
|
+
id: sessionId,
|
|
254
|
+
user_id: userId,
|
|
255
|
+
ip_address: req.ip || req.socket.remoteAddress,
|
|
256
|
+
user_agent: req.get('user-agent'),
|
|
257
|
+
expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
|
|
258
|
+
last_active_at: now,
|
|
259
|
+
created_at: now,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
res.cookie(SESSION_COOKIE_NAME, sessionId, {
|
|
263
|
+
httpOnly: true,
|
|
264
|
+
secure: isProduction,
|
|
265
|
+
sameSite: 'lax',
|
|
266
|
+
maxAge: config.session_ttl_seconds * 1000,
|
|
267
|
+
path: '/',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
res.status(201).json({
|
|
271
|
+
user: { id: userId, email, display_name: displayName },
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// POST /auth/login — sign in with email + password
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
router.post('/login', async (req: Request, res: Response) => {
|
|
279
|
+
const { email, password } = req.body || {};
|
|
280
|
+
|
|
281
|
+
if (!email || !password) {
|
|
282
|
+
res.status(400).json({ error: 'email and password are required' });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const user = userStore.getByEmail(email);
|
|
287
|
+
if (!user || !user.password_hash) {
|
|
288
|
+
res.status(401).json({ error: 'Invalid email or password' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!(await verifyPassword(password, user.password_hash))) {
|
|
293
|
+
res.status(401).json({ error: 'Invalid email or password' });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (user.status !== 'active') {
|
|
298
|
+
res.status(403).json({ error: 'Account is suspended' });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const now = new Date().toISOString();
|
|
303
|
+
const sessionId = generateSessionId();
|
|
304
|
+
|
|
305
|
+
const memberships = workspaceMemberStore.getByUser(user.id);
|
|
306
|
+
const workspaceId = memberships.length > 0 ? memberships[0].workspace_id : undefined;
|
|
307
|
+
|
|
308
|
+
sessionStore.create({
|
|
309
|
+
id: sessionId,
|
|
310
|
+
user_id: user.id,
|
|
311
|
+
workspace_id: workspaceId,
|
|
312
|
+
ip_address: req.ip || req.socket.remoteAddress,
|
|
313
|
+
user_agent: req.get('user-agent'),
|
|
314
|
+
expires_at: new Date(Date.now() + config.session_ttl_seconds * 1000).toISOString(),
|
|
315
|
+
last_active_at: now,
|
|
316
|
+
created_at: now,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
res.cookie(SESSION_COOKIE_NAME, sessionId, {
|
|
320
|
+
httpOnly: true,
|
|
321
|
+
secure: isProduction,
|
|
322
|
+
sameSite: 'lax',
|
|
323
|
+
maxAge: config.session_ttl_seconds * 1000,
|
|
324
|
+
path: '/',
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
res.json({
|
|
328
|
+
user: {
|
|
329
|
+
id: user.id,
|
|
330
|
+
email: user.email,
|
|
331
|
+
display_name: user.display_name,
|
|
332
|
+
onboarding_completed: user.onboarding_completed,
|
|
333
|
+
default_workspace_id: workspaceId,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// GET /auth/me — return current user + session info
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
router.get('/me', (req: Request, res: Response) => {
|
|
342
|
+
const sessionUser = (req as any).sessionUser;
|
|
343
|
+
if (!sessionUser) {
|
|
344
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const oauthAccounts = oauthAccountStore.getByUserId(sessionUser.id);
|
|
349
|
+
const memberships = workspaceMemberStore.getByUser(sessionUser.id);
|
|
350
|
+
const workspaces = memberships.map(m => {
|
|
351
|
+
const ws = workspaceStore.getById(m.workspace_id);
|
|
352
|
+
return ws ? { ...ws, role: m.role } : null;
|
|
353
|
+
}).filter(Boolean);
|
|
354
|
+
|
|
355
|
+
// Pick the first workspace as default
|
|
356
|
+
const defaultWorkspaceId = workspaces.length > 0 ? (workspaces[0] as any).id : undefined;
|
|
357
|
+
|
|
358
|
+
res.json({
|
|
359
|
+
user: {
|
|
360
|
+
id: sessionUser.id,
|
|
361
|
+
email: sessionUser.email,
|
|
362
|
+
display_name: sessionUser.display_name,
|
|
363
|
+
avatar_url: sessionUser.avatar_url,
|
|
364
|
+
status: sessionUser.status,
|
|
365
|
+
onboarding_completed: sessionUser.onboarding_completed,
|
|
366
|
+
default_workspace_id: defaultWorkspaceId,
|
|
367
|
+
created_at: sessionUser.created_at,
|
|
368
|
+
},
|
|
369
|
+
providers: oauthAccounts.map(a => ({
|
|
370
|
+
provider: a.provider,
|
|
371
|
+
email: a.provider_email,
|
|
372
|
+
})),
|
|
373
|
+
workspaces,
|
|
374
|
+
session: {
|
|
375
|
+
workspace_id: (req as any).sessionData?.workspace_id,
|
|
376
|
+
expires_at: (req as any).sessionData?.expires_at,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// PUT /auth/password — change password for current user (or admin sets for another user by email)
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
router.put('/password', async (req: Request, res: Response) => {
|
|
385
|
+
const sessionUser = (req as any).sessionUser;
|
|
386
|
+
if (!sessionUser) {
|
|
387
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { new_password, email } = req.body || {};
|
|
392
|
+
if (!new_password || new_password.length < 6) {
|
|
393
|
+
res.status(400).json({ error: 'new_password must be at least 6 characters' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// If email is provided and differs from session user, check admin
|
|
398
|
+
let targetUser = sessionUser;
|
|
399
|
+
if (email && email !== sessionUser.email) {
|
|
400
|
+
// Only workspace owners can reset other users' passwords
|
|
401
|
+
const memberships = workspaceMemberStore.getByUser(sessionUser.id);
|
|
402
|
+
const isOwner = memberships.some(m => m.role === 'owner');
|
|
403
|
+
if (!isOwner) {
|
|
404
|
+
res.status(403).json({ error: 'Only workspace owners can reset other users passwords' });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const found = userStore.getByEmail(email);
|
|
408
|
+
if (!found) {
|
|
409
|
+
res.status(404).json({ error: 'User not found' });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
targetUser = found;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const newHash = await hashPassword(new_password);
|
|
416
|
+
userStore.update(targetUser.id, { password_hash: newHash, updated_at: new Date().toISOString() });
|
|
417
|
+
|
|
418
|
+
res.json({ ok: true, email: targetUser.email });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// DELETE /auth/users/:id — delete a user and all related data (self only)
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
router.delete('/users/:id', (req: Request, res: Response) => {
|
|
425
|
+
const sessionUser = (req as any).sessionUser;
|
|
426
|
+
if (!sessionUser) {
|
|
427
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const targetId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
432
|
+
|
|
433
|
+
// Only allow deleting yourself (no admin role needed)
|
|
434
|
+
if (sessionUser.id !== targetId) {
|
|
435
|
+
res.status(403).json({ error: 'Can only delete your own account' });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Delete workspace memberships and owned workspaces
|
|
440
|
+
const memberships = workspaceMemberStore.getByUser(targetId);
|
|
441
|
+
for (const m of memberships) {
|
|
442
|
+
// If sole owner of workspace, delete the workspace too
|
|
443
|
+
const wsMembers = workspaceMemberStore.getByWorkspace(m.workspace_id);
|
|
444
|
+
const otherOwners = wsMembers.filter(wm => wm.user_id !== targetId && wm.role === 'owner');
|
|
445
|
+
if (otherOwners.length === 0) {
|
|
446
|
+
// Delete all members of this workspace
|
|
447
|
+
for (const wm of wsMembers) {
|
|
448
|
+
workspaceMemberStore.delete(wm.id);
|
|
449
|
+
}
|
|
450
|
+
// Delete workspace API keys
|
|
451
|
+
if (userApiKeyStore) {
|
|
452
|
+
const keys = userApiKeyStore.getByWorkspace(m.workspace_id);
|
|
453
|
+
for (const k of keys) {
|
|
454
|
+
userApiKeyStore.delete(k.id);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
workspaceStore.delete(m.workspace_id);
|
|
458
|
+
} else {
|
|
459
|
+
workspaceMemberStore.delete(m.id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Delete OAuth accounts
|
|
464
|
+
const oauthAccounts = oauthAccountStore.getByUserId(targetId);
|
|
465
|
+
for (const oa of oauthAccounts) {
|
|
466
|
+
oauthAccountStore.delete(oa.id);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Delete user API keys
|
|
470
|
+
if (userApiKeyStore) {
|
|
471
|
+
const keys = userApiKeyStore.getByUser(targetId);
|
|
472
|
+
for (const k of keys) {
|
|
473
|
+
userApiKeyStore.delete(k.id);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Clear session
|
|
478
|
+
const sessionId = req.cookies?.[SESSION_COOKIE_NAME];
|
|
479
|
+
if (sessionId) {
|
|
480
|
+
sessionStore.delete(sessionId);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Delete user
|
|
484
|
+
userStore.delete(targetId);
|
|
485
|
+
|
|
486
|
+
res.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
|
|
487
|
+
res.json({ status: 'ok', deleted_user_id: targetId });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// POST /auth/logout — clear session
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
router.post('/logout', (req: Request, res: Response) => {
|
|
494
|
+
const sessionId = req.cookies?.[SESSION_COOKIE_NAME];
|
|
495
|
+
if (sessionId) {
|
|
496
|
+
sessionStore.delete(sessionId);
|
|
497
|
+
}
|
|
498
|
+
res.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
|
|
499
|
+
res.json({ status: 'ok' });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Helper: find or create user from OAuth profile
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
function findOrCreateUser(profile: OAuthProfile, accessToken: string, refreshToken?: string): {
|
|
506
|
+
user: any;
|
|
507
|
+
isNewUser: boolean;
|
|
508
|
+
needsLinking?: boolean;
|
|
509
|
+
} {
|
|
510
|
+
const now = new Date().toISOString();
|
|
511
|
+
|
|
512
|
+
// Check if OAuth account already exists
|
|
513
|
+
const existingOAuth = oauthAccountStore.getByProvider(profile.provider, profile.provider_user_id);
|
|
514
|
+
if (existingOAuth) {
|
|
515
|
+
// Update tokens
|
|
516
|
+
oauthAccountStore.update(existingOAuth.id, {
|
|
517
|
+
access_token_encrypted: encryptToken(accessToken, config.session_secret),
|
|
518
|
+
refresh_token_encrypted: refreshToken ? encryptToken(refreshToken, config.session_secret) : undefined,
|
|
519
|
+
updated_at: now,
|
|
520
|
+
});
|
|
521
|
+
const user = userStore.getById(existingOAuth.user_id)!;
|
|
522
|
+
return { user, isNewUser: false };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check if user exists with same email — do NOT auto-link
|
|
526
|
+
const existingUser = userStore.getByEmail(profile.email);
|
|
527
|
+
if (existingUser) {
|
|
528
|
+
return { user: existingUser, isNewUser: false, needsLinking: true };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Create new user
|
|
532
|
+
const user = {
|
|
533
|
+
id: randomUUID(),
|
|
534
|
+
email: profile.email,
|
|
535
|
+
display_name: profile.display_name,
|
|
536
|
+
avatar_url: profile.avatar_url,
|
|
537
|
+
status: 'active' as const,
|
|
538
|
+
onboarding_completed: false,
|
|
539
|
+
created_at: now,
|
|
540
|
+
updated_at: now,
|
|
541
|
+
};
|
|
542
|
+
userStore.create(user);
|
|
543
|
+
|
|
544
|
+
// Create OAuth account link for new users only
|
|
545
|
+
oauthAccountStore.create({
|
|
546
|
+
id: randomUUID(),
|
|
547
|
+
user_id: user.id,
|
|
548
|
+
provider: profile.provider,
|
|
549
|
+
provider_user_id: profile.provider_user_id,
|
|
550
|
+
provider_email: profile.email,
|
|
551
|
+
access_token_encrypted: encryptToken(accessToken, config.session_secret),
|
|
552
|
+
refresh_token_encrypted: refreshToken ? encryptToken(refreshToken, config.session_secret) : undefined,
|
|
553
|
+
created_at: now,
|
|
554
|
+
updated_at: now,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return { user, isNewUser: true };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return router;
|
|
561
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { OAuthFlowState, OAuthConfig } from '../types/user';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a cryptographically random session ID (256-bit).
|
|
6
|
+
*/
|
|
7
|
+
export function generateSessionId(): string {
|
|
8
|
+
return crypto.randomBytes(32).toString('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// OAuth state cookie (encrypted with AES-256-GCM)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function deriveKey(secret: string, context: string = 'palaryn-session'): Buffer {
|
|
16
|
+
return Buffer.from(crypto.hkdfSync('sha256', Buffer.from(secret, 'utf-8'), Buffer.alloc(32, 0), Buffer.from(context, 'utf-8'), 32));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encrypt an OAuth flow state object into a cookie-safe string.
|
|
21
|
+
*/
|
|
22
|
+
export function encryptState(state: OAuthFlowState, secret: string): string {
|
|
23
|
+
const key = deriveKey(secret);
|
|
24
|
+
const iv = crypto.randomBytes(12);
|
|
25
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
26
|
+
const plaintext = JSON.stringify(state);
|
|
27
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
28
|
+
const tag = cipher.getAuthTag();
|
|
29
|
+
// Format: base64url(iv + tag + ciphertext)
|
|
30
|
+
return Buffer.concat([iv, tag, encrypted]).toString('base64url');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decrypt an OAuth flow state from a cookie value.
|
|
35
|
+
*/
|
|
36
|
+
export function decryptState(encrypted: string, secret: string): OAuthFlowState {
|
|
37
|
+
const key = deriveKey(secret);
|
|
38
|
+
const buf = Buffer.from(encrypted, 'base64url');
|
|
39
|
+
const iv = buf.subarray(0, 12);
|
|
40
|
+
const tag = buf.subarray(12, 28);
|
|
41
|
+
const ciphertext = buf.subarray(28);
|
|
42
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
43
|
+
decipher.setAuthTag(tag);
|
|
44
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8');
|
|
45
|
+
return JSON.parse(plaintext);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Token encryption (AES-256-GCM for storing OAuth tokens at rest)
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export function encryptToken(token: string, secret: string): string {
|
|
53
|
+
const key = deriveKey(secret);
|
|
54
|
+
const iv = crypto.randomBytes(12);
|
|
55
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
56
|
+
const encrypted = Buffer.concat([cipher.update(token, 'utf-8'), cipher.final()]);
|
|
57
|
+
const tag = cipher.getAuthTag();
|
|
58
|
+
return Buffer.concat([iv, tag, encrypted]).toString('base64');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function decryptToken(encryptedToken: string, secret: string): string {
|
|
62
|
+
const key = deriveKey(secret);
|
|
63
|
+
const buf = Buffer.from(encryptedToken, 'base64');
|
|
64
|
+
const iv = buf.subarray(0, 12);
|
|
65
|
+
const tag = buf.subarray(12, 28);
|
|
66
|
+
const ciphertext = buf.subarray(28);
|
|
67
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
68
|
+
decipher.setAuthTag(tag);
|
|
69
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Cookie helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export interface SessionCookieOptions {
|
|
77
|
+
name: string;
|
|
78
|
+
maxAge: number; // seconds
|
|
79
|
+
secure: boolean;
|
|
80
|
+
domain?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const SESSION_COOKIE_NAME = 'pn_session';
|
|
84
|
+
export const STATE_COOKIE_NAME = 'pn_oauth_state';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { StripeClient } from './stripe-client';
|
|
2
|
+
export { PlanEnforcer } from './plan-enforcer';
|
|
3
|
+
export { WebhookHandler } from './webhook-handler';
|
|
4
|
+
export { createWebhookRouter } from './webhook-routes';
|
|
5
|
+
export { createBillingRouter } from './routes';
|
|
6
|
+
export type { BillingRouteDeps } from './routes';
|