bhooai-nexus 0.1.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 +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/** Types mirroring contracts/ai-openapi.yaml — the Node↔Python AI boundary. */
|
|
2
|
+
|
|
3
|
+
export type Provider = 'openai' | 'ollama' | 'auto';
|
|
4
|
+
export type Role = 'system' | 'user' | 'assistant' | 'tool';
|
|
5
|
+
|
|
6
|
+
export interface Message {
|
|
7
|
+
role: Role;
|
|
8
|
+
content: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ChatCompletionRequest {
|
|
13
|
+
model: string;
|
|
14
|
+
messages: Message[];
|
|
15
|
+
stream?: boolean;
|
|
16
|
+
temperature?: number;
|
|
17
|
+
max_tokens?: number;
|
|
18
|
+
provider?: Provider;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Choice {
|
|
22
|
+
index: number;
|
|
23
|
+
message: Message;
|
|
24
|
+
finish_reason: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Usage {
|
|
28
|
+
prompt_tokens?: number;
|
|
29
|
+
completion_tokens?: number;
|
|
30
|
+
total_tokens?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ChatCompletionResponse {
|
|
34
|
+
id?: string;
|
|
35
|
+
model: string;
|
|
36
|
+
provider?: string;
|
|
37
|
+
choices: Choice[];
|
|
38
|
+
usage?: Usage;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Delta {
|
|
42
|
+
role?: Role;
|
|
43
|
+
content?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ChunkChoice {
|
|
47
|
+
index: number;
|
|
48
|
+
delta: Delta;
|
|
49
|
+
finish_reason: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChatCompletionChunk {
|
|
53
|
+
id?: string;
|
|
54
|
+
model: string;
|
|
55
|
+
provider?: string;
|
|
56
|
+
choices: ChunkChoice[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EmbeddingRequest {
|
|
60
|
+
model: string;
|
|
61
|
+
input: string | string[];
|
|
62
|
+
provider?: Provider;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EmbeddingData {
|
|
66
|
+
index: number;
|
|
67
|
+
embedding: number[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface EmbeddingResponse {
|
|
71
|
+
model: string;
|
|
72
|
+
provider?: string;
|
|
73
|
+
data: EmbeddingData[];
|
|
74
|
+
usage?: Usage;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ModelInfo {
|
|
78
|
+
id: string;
|
|
79
|
+
owned_by?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ModelsResponse {
|
|
83
|
+
provider?: string;
|
|
84
|
+
data: ModelInfo[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AiClientOptions {
|
|
88
|
+
/** AI server base URL, e.g. http://localhost:8000. */
|
|
89
|
+
serverUrl: string;
|
|
90
|
+
/** Request timeout in ms (default 60000). */
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
/** Max retry attempts on network/5xx errors (default 2). */
|
|
93
|
+
maxRetries?: number;
|
|
94
|
+
/** Base delay for exponential backoff in ms (default 500). */
|
|
95
|
+
retryDelayMs?: number;
|
|
96
|
+
/** Optional Bearer token sent to the AI server. */
|
|
97
|
+
authToken?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Image generation (local Stable Diffusion HTTP server) ──────────────────
|
|
101
|
+
// OpenAI-compatible /images/generations surface, plus the agentic loop.
|
|
102
|
+
|
|
103
|
+
export interface ImageGenRequest {
|
|
104
|
+
prompt: string;
|
|
105
|
+
n?: number;
|
|
106
|
+
size?: string;
|
|
107
|
+
model?: string;
|
|
108
|
+
negative_prompt?: string;
|
|
109
|
+
steps?: number;
|
|
110
|
+
sampler?: string;
|
|
111
|
+
seed?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ImageGenData {
|
|
115
|
+
b64_json?: string;
|
|
116
|
+
url?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ImageGenMeta {
|
|
120
|
+
prompt: string;
|
|
121
|
+
negative_prompt: string;
|
|
122
|
+
model: string;
|
|
123
|
+
steps: number;
|
|
124
|
+
width: number;
|
|
125
|
+
height: number;
|
|
126
|
+
sampler: string;
|
|
127
|
+
seed: number | null;
|
|
128
|
+
n: number;
|
|
129
|
+
api: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ImageGenResponse {
|
|
133
|
+
created: number;
|
|
134
|
+
data: ImageGenData[];
|
|
135
|
+
meta?: ImageGenMeta;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ImageGenModelsResponse {
|
|
139
|
+
api: string;
|
|
140
|
+
data: Array<{ id: string; title: string }>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Agentic image loop ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface AgentImageRequest {
|
|
146
|
+
/** The user's rough idea, e.g. "a cyberpunk fox in neon rain". */
|
|
147
|
+
idea: string;
|
|
148
|
+
/** Max agent iterations (default from server config). */
|
|
149
|
+
max_iterations?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** SSE event emitted by POST /agent/image. Discriminated by `type`. */
|
|
153
|
+
export type AgentImageEvent =
|
|
154
|
+
| { type: 'thinking'; content: string; iteration: number }
|
|
155
|
+
| { type: 'tool'; name: string; args: Record<string, unknown>; iteration: number }
|
|
156
|
+
| { type: 'tool_error'; name: string; message: string; iteration: number }
|
|
157
|
+
| { type: 'image'; b64: string; prompt: string; iteration: number }
|
|
158
|
+
| { type: 'error'; message: string }
|
|
159
|
+
| AgentImageDoneEvent;
|
|
160
|
+
|
|
161
|
+
export interface AgentImageDoneEvent {
|
|
162
|
+
type: 'done';
|
|
163
|
+
image_b64: string;
|
|
164
|
+
prompt: string;
|
|
165
|
+
negative_prompt: string;
|
|
166
|
+
style?: string | null;
|
|
167
|
+
aspect_ratio?: string | null;
|
|
168
|
+
model: string;
|
|
169
|
+
steps: number;
|
|
170
|
+
sampler?: string | null;
|
|
171
|
+
seed?: number | null;
|
|
172
|
+
iterations: number;
|
|
173
|
+
critique_log: Array<{ score?: number; feedback?: string; suggestions?: string[] }>;
|
|
174
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@bhooai/nexus-core": "^0.1.0",
|
|
14
|
+
"bcryptjs": "^2.4.3",
|
|
15
|
+
"jose": "^5.9.6"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.5.0",
|
|
19
|
+
"typescript": "^5.6.2",
|
|
20
|
+
"vitest": "^2.1.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { signAccessToken, verifyToken, type JwtOptions, type TokenPayload } from './jwt.js';
|
|
3
|
+
import type { SessionStore, Session } from './session.js';
|
|
4
|
+
import { AuthenticationError } from '../../nexus-core/src/index.js';
|
|
5
|
+
|
|
6
|
+
export interface TokenPair {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
refreshToken: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
/** Refresh-token expiry (ms epoch). */
|
|
11
|
+
refreshExpiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LoginInput {
|
|
15
|
+
userId: string;
|
|
16
|
+
roles: string[];
|
|
17
|
+
meta?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* High-level auth orchestrator. The app verifies credentials (via
|
|
22
|
+
* `verifyPassword` + its user store) and calls `login()` to mint a session and
|
|
23
|
+
* token pair. Refresh tokens are rotated on every refresh and bound to a
|
|
24
|
+
* session family, so reuse of a previously-rotated token revokes the whole
|
|
25
|
+
* family (refresh-token theft detection).
|
|
26
|
+
*/
|
|
27
|
+
export class AuthService {
|
|
28
|
+
constructor(
|
|
29
|
+
private jwt: JwtOptions,
|
|
30
|
+
private sessions: SessionStore,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
/** Create a session and return an access + refresh token pair. */
|
|
34
|
+
async login(input: LoginInput): Promise<TokenPair> {
|
|
35
|
+
const session = await this.sessions.create(input.userId, input.roles, input.meta);
|
|
36
|
+
return this.mintPair(session);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Verify a refresh token, rotate it (new jti), and return a fresh pair.
|
|
41
|
+
* If the presented refresh token's jti no longer matches the session's
|
|
42
|
+
* current jti, the token has been reused after rotation → revoke the family.
|
|
43
|
+
*/
|
|
44
|
+
async refresh(refreshToken: string): Promise<TokenPair> {
|
|
45
|
+
let payload: TokenPayload;
|
|
46
|
+
try {
|
|
47
|
+
payload = await verifyToken(refreshToken, this.jwt);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new AuthenticationError('Invalid refresh token');
|
|
50
|
+
}
|
|
51
|
+
if (payload.kind !== 'refresh' || !payload.sid) {
|
|
52
|
+
throw new AuthenticationError('Not a refresh token');
|
|
53
|
+
}
|
|
54
|
+
const session = await this.sessions.get(payload.sid);
|
|
55
|
+
if (!session) throw new AuthenticationError('Session expired');
|
|
56
|
+
|
|
57
|
+
// Reuse detection: the jti on the session must match the presented token.
|
|
58
|
+
if (session.currentJti && payload.jti && session.currentJti !== payload.jti) {
|
|
59
|
+
await this.sessions.destroyFamily(session.familyId);
|
|
60
|
+
throw new AuthenticationError('Refresh token reuse detected; session revoked');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.mintPair(session);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** End a single session (logout). */
|
|
67
|
+
async logout(sessionId: string): Promise<void> {
|
|
68
|
+
await this.sessions.destroy(sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** End every session for a user (logout everywhere). */
|
|
72
|
+
async logoutAll(userId: string): Promise<void> {
|
|
73
|
+
await this.sessions.destroyAllForUser(userId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Verify an access token and return its payload (for the auth middleware). */
|
|
77
|
+
async verifyAccessToken(accessToken: string): Promise<TokenPayload> {
|
|
78
|
+
try {
|
|
79
|
+
const payload = await verifyToken(accessToken, this.jwt);
|
|
80
|
+
if (payload.kind && payload.kind !== 'access') {
|
|
81
|
+
throw new AuthenticationError('Not an access token');
|
|
82
|
+
}
|
|
83
|
+
return payload;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (e instanceof AuthenticationError) throw e;
|
|
86
|
+
throw new AuthenticationError('Invalid access token');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async mintPair(session: Session): Promise<TokenPair> {
|
|
91
|
+
const jti = randomBytes(12).toString('base64url');
|
|
92
|
+
// Record the issued refresh jti on the session for reuse detection.
|
|
93
|
+
await this.sessions.update(session.id, { currentJti: jti });
|
|
94
|
+
const access = await signAccessToken(session.userId, session.roles, this.jwt, session.id);
|
|
95
|
+
const refresh = await this.signRefreshWithJti(session, jti);
|
|
96
|
+
const refreshExpiresAt = Date.now() + (this.jwt.refreshTtl ?? 60 * 60 * 24 * 7) * 1000;
|
|
97
|
+
return { accessToken: access, refreshToken: refresh, sessionId: session.id, refreshExpiresAt };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async signRefreshWithJti(session: Session, jti: string): Promise<string> {
|
|
101
|
+
// Re-use the jose SignJWT path by importing here to inject jti.
|
|
102
|
+
const { SignJWT } = await import('jose');
|
|
103
|
+
const ttl = this.jwt.refreshTtl ?? 60 * 60 * 24 * 7;
|
|
104
|
+
const secret =
|
|
105
|
+
typeof this.jwt.secret === 'string' ? new TextEncoder().encode(this.jwt.secret) : this.jwt.secret;
|
|
106
|
+
const builder = new SignJWT({ kind: 'refresh', roles: session.roles, sid: session.id })
|
|
107
|
+
.setProtectedHeader({ alg: this.jwt.algorithm ?? 'HS256' })
|
|
108
|
+
.setSubject(session.userId)
|
|
109
|
+
.setJti(jti)
|
|
110
|
+
.setIssuedAt()
|
|
111
|
+
.setExpirationTime(`${ttl}s`);
|
|
112
|
+
if (this.jwt.issuer) builder.setIssuer(this.jwt.issuer);
|
|
113
|
+
if (this.jwt.audience) builder.setAudience(this.jwt.audience);
|
|
114
|
+
return builder.sign(secret);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare module 'bcryptjs' {
|
|
2
|
+
interface Bcrypt {
|
|
3
|
+
genSalt(rounds: number): Promise<string>;
|
|
4
|
+
hash(plaintext: string, salt: string): Promise<string>;
|
|
5
|
+
compare(plaintext: string, hash: string): Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const bcrypt: Bcrypt;
|
|
9
|
+
export default bcrypt;
|
|
10
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Middleware } from '../../nexus-core/src/http/index.js';
|
|
2
|
+
|
|
3
|
+
export interface CorsOptions {
|
|
4
|
+
/** Allowed origin(s): a string, an array, or true to reflect any origin. */
|
|
5
|
+
origin?: string | string[] | boolean;
|
|
6
|
+
/** Allowed methods. */
|
|
7
|
+
methods?: string[];
|
|
8
|
+
/** Allowed request headers. */
|
|
9
|
+
allowedHeaders?: string[];
|
|
10
|
+
/** Headers exposed to the browser. */
|
|
11
|
+
exposedHeaders?: string[];
|
|
12
|
+
/** Allow cookies / credentials. When true, origin cannot be "*". */
|
|
13
|
+
credentials?: boolean;
|
|
14
|
+
/** Preflight cache max age in seconds. */
|
|
15
|
+
maxAge?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inbuilt CORS middleware. Owns the OPTIONS preflight path entirely, sets
|
|
22
|
+
* `Vary: Origin` for correct cache behavior, and enforces the credentialed
|
|
23
|
+
* rule (no wildcard origin when credentials are enabled).
|
|
24
|
+
*/
|
|
25
|
+
export function cors(options: CorsOptions = {}): Middleware {
|
|
26
|
+
const opts = {
|
|
27
|
+
origin: options.origin ?? true,
|
|
28
|
+
methods: options.methods ?? DEFAULT_METHODS,
|
|
29
|
+
allowedHeaders: options.allowedHeaders,
|
|
30
|
+
exposedHeaders: options.exposedHeaders,
|
|
31
|
+
credentials: options.credentials ?? false,
|
|
32
|
+
maxAge: options.maxAge ?? 600,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return async (ctx, next) => {
|
|
36
|
+
const reqOrigin = (ctx.headers['origin'] as string) ?? '';
|
|
37
|
+
const allowedOrigin = resolveOrigin(opts.origin, reqOrigin, opts.credentials);
|
|
38
|
+
|
|
39
|
+
// Always vary by origin so caches don't leak the wrong CORS headers.
|
|
40
|
+
appendVary(ctx, 'Origin');
|
|
41
|
+
|
|
42
|
+
if (allowedOrigin) {
|
|
43
|
+
ctx.setHeader('access-control-allow-origin', allowedOrigin);
|
|
44
|
+
if (opts.credentials) ctx.setHeader('access-control-allow-credentials', 'true');
|
|
45
|
+
if (opts.exposedHeaders?.length) {
|
|
46
|
+
ctx.setHeader('access-control-expose-headers', opts.exposedHeaders.join(', '));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (ctx.method === 'OPTIONS' && ctx.headers['access-control-request-method']) {
|
|
51
|
+
// Preflight: short-circuit
|
|
52
|
+
ctx.setHeader('access-control-allow-methods', opts.methods.join(', '));
|
|
53
|
+
const reqHeaders = (ctx.headers['access-control-request-headers'] as string) ?? '';
|
|
54
|
+
ctx.setHeader('access-control-allow-headers', (opts.allowedHeaders ?? reqHeaders) || '*');
|
|
55
|
+
ctx.setHeader('access-control-max-age', String(opts.maxAge));
|
|
56
|
+
ctx.status(204);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveOrigin(
|
|
65
|
+
config: string | string[] | boolean,
|
|
66
|
+
requestOrigin: string,
|
|
67
|
+
credentials: boolean,
|
|
68
|
+
): string | undefined {
|
|
69
|
+
if (config === true) {
|
|
70
|
+
// Reflect the request origin (works with credentials); no "*" when credentialed.
|
|
71
|
+
return requestOrigin || (credentials ? undefined : '*');
|
|
72
|
+
}
|
|
73
|
+
if (typeof config === 'string') {
|
|
74
|
+
if (config === '*') return credentials ? requestOrigin || undefined : '*';
|
|
75
|
+
return config === requestOrigin ? config : undefined;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(config)) {
|
|
78
|
+
return config.includes(requestOrigin) ? requestOrigin : undefined;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function appendVary(ctx: Parameters<Middleware>[0], value: string): void {
|
|
84
|
+
const existing = ctx.res.getHeader('vary');
|
|
85
|
+
if (existing === undefined) {
|
|
86
|
+
ctx.setHeader('vary', value);
|
|
87
|
+
} else {
|
|
88
|
+
const merged = `${existing}, ${value}`;
|
|
89
|
+
ctx.setHeader('vary', merged);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import type { Middleware, RequestContext } from '../../nexus-core/src/http/index.js';
|
|
3
|
+
import { AuthenticationError } from '../../nexus-core/src/index.js';
|
|
4
|
+
|
|
5
|
+
export interface CsrfOptions {
|
|
6
|
+
/** Cookie name holding the double-submit token. */
|
|
7
|
+
cookieName?: string;
|
|
8
|
+
/** Header / body field name carrying the token on unsafe requests. */
|
|
9
|
+
tokenName?: string;
|
|
10
|
+
/** Trusted origin hosts (for the origin/referer check). If omitted, origin check is skipped. */
|
|
11
|
+
trustedOrigins?: string[];
|
|
12
|
+
/** Methods exempt from CSRF (safe methods). */
|
|
13
|
+
safeMethods?: string[];
|
|
14
|
+
/** Cookie attributes. */
|
|
15
|
+
cookie?: { path?: string; secure?: boolean; sameSite?: 'strict' | 'lax' | 'none'; domain?: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SAFE = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inbuilt CSRF protection using the double-submit cookie pattern plus an
|
|
22
|
+
* origin/referer check on unsafe requests. Safe methods are exempt.
|
|
23
|
+
*
|
|
24
|
+
* On a safe request, a token cookie is set (rotated each request) and exposed
|
|
25
|
+
* via `ctx.state.csrfToken` so handlers can return it to clients. On unsafe
|
|
26
|
+
* requests, the header token must equal the cookie token.
|
|
27
|
+
*/
|
|
28
|
+
export function csrf(options: CsrfOptions = {}): Middleware {
|
|
29
|
+
const cookieName = options.cookieName ?? 'nexus_csrf';
|
|
30
|
+
const tokenName = options.tokenName ?? 'x-csrf-token';
|
|
31
|
+
const trusted = options.trustedOrigins ?? [];
|
|
32
|
+
const safeMethods = new Set(options.safeMethods ?? [...SAFE]);
|
|
33
|
+
const cookieOpts = options.cookie ?? { path: '/', sameSite: 'lax', secure: false };
|
|
34
|
+
|
|
35
|
+
return async (ctx, next) => {
|
|
36
|
+
if (safeMethods.has(ctx.method)) {
|
|
37
|
+
// Reuse an existing cookie token so concurrent safe requests (e.g. the
|
|
38
|
+
// 5-second cluster overview poll) don't rotate the token out from under
|
|
39
|
+
// a pending unsafe request — that race produced "Unauthorized" on POSTs.
|
|
40
|
+
const existing = readCookie(ctx, cookieName);
|
|
41
|
+
if (existing) {
|
|
42
|
+
ctx.state.csrfToken = existing;
|
|
43
|
+
} else {
|
|
44
|
+
ctx.state.csrfToken = issueToken(ctx, cookieName, cookieOpts);
|
|
45
|
+
}
|
|
46
|
+
await next();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Unsafe method: validate origin/referer first.
|
|
51
|
+
checkOrigin(ctx, trusted);
|
|
52
|
+
|
|
53
|
+
// Then double-submit: header token must match cookie token.
|
|
54
|
+
const cookieToken = readCookie(ctx, cookieName);
|
|
55
|
+
const sentToken =
|
|
56
|
+
(ctx.headers[tokenName] as string) ??
|
|
57
|
+
(ctx.body && typeof ctx.body === 'object' ? (ctx.body as Record<string, unknown>)[tokenName] as string : undefined);
|
|
58
|
+
|
|
59
|
+
if (!cookieToken || !sentToken || !timingSafeEqual(cookieToken, sentToken)) {
|
|
60
|
+
throw new AuthenticationError('Invalid CSRF token');
|
|
61
|
+
}
|
|
62
|
+
await next();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Helper for handlers/endpoints to mint a token (e.g. a /csrf-token route). */
|
|
67
|
+
export function issueCsrfToken(ctx: RequestContext, options: CsrfOptions = {}): string {
|
|
68
|
+
return issueToken(ctx, options.cookieName ?? 'nexus_csrf', options.cookie ?? { path: '/', sameSite: 'lax', secure: false });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** CSRF + origin check for the WebSocket upgrade request (no CORS preflight). */
|
|
72
|
+
export function checkWsUpgrade(headers: Record<string, string | string[] | undefined>, options: CsrfOptions = {}): void {
|
|
73
|
+
const trusted = options.trustedOrigins ?? [];
|
|
74
|
+
if (trusted.length) {
|
|
75
|
+
const origin = (headers['origin'] as string) ?? '';
|
|
76
|
+
if (!origin || !isTrustedOrigin(origin, trusted)) {
|
|
77
|
+
throw new AuthenticationError('Untrusted WebSocket origin');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const cookieToken = readCookieFromHeader((headers['cookie'] as string) ?? '', options.cookieName ?? 'nexus_csrf');
|
|
81
|
+
const queryToken = (headers['sec-websocket-protocol'] as string)?.split(',').map((s) => s.trim())[0];
|
|
82
|
+
if (!cookieToken || !queryToken || !timingSafeEqual(cookieToken, queryToken)) {
|
|
83
|
+
throw new AuthenticationError('Invalid WebSocket CSRF token');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function issueToken(
|
|
88
|
+
ctx: RequestContext,
|
|
89
|
+
cookieName: string,
|
|
90
|
+
opts: NonNullable<CsrfOptions['cookie']>,
|
|
91
|
+
): string {
|
|
92
|
+
const token = randomBytes(24).toString('base64url');
|
|
93
|
+
const parts = [
|
|
94
|
+
`${cookieName}=${token}`,
|
|
95
|
+
`Path=${opts.path ?? '/'}`,
|
|
96
|
+
`SameSite=${opts.sameSite ?? 'lax'}`,
|
|
97
|
+
opts.secure ? 'Secure' : '',
|
|
98
|
+
opts.domain ? `Domain=${opts.domain}` : '',
|
|
99
|
+
'HttpOnly',
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
ctx.setHeader('set-cookie', parts.join('; '));
|
|
102
|
+
return token;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readCookie(ctx: RequestContext, name: string): string | undefined {
|
|
106
|
+
return readCookieFromHeader((ctx.headers['cookie'] as string) ?? '', name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readCookieFromHeader(header: string, name: string): string | undefined {
|
|
110
|
+
for (const part of header.split(';')) {
|
|
111
|
+
const eq = part.indexOf('=');
|
|
112
|
+
const k = part.slice(0, eq).trim();
|
|
113
|
+
if (k === name) return part.slice(eq + 1).trim();
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkOrigin(ctx: RequestContext, trusted: string[]): void {
|
|
119
|
+
if (trusted.length === 0) return;
|
|
120
|
+
const origin = (ctx.headers['origin'] as string) ?? (ctx.headers['referer'] as string) ?? '';
|
|
121
|
+
if (!origin || !isTrustedOrigin(origin, trusted)) {
|
|
122
|
+
throw new AuthenticationError('Untrusted request origin');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isTrustedOrigin(origin: string, trusted: string[]): boolean {
|
|
127
|
+
let hostname: string;
|
|
128
|
+
let host: string;
|
|
129
|
+
try {
|
|
130
|
+
const u = new URL(origin);
|
|
131
|
+
hostname = u.hostname;
|
|
132
|
+
host = u.host;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
// Loopback origins (localhost/127.0.0.1) are safe from cross-site forgery
|
|
137
|
+
// regardless of port — Docker / port-forwarding commonly remaps host ports
|
|
138
|
+
// (e.g. container :3001 published on the host as :3011), so a strict
|
|
139
|
+
// host:port match would wrongly reject the admin/frontend dev servers.
|
|
140
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
|
141
|
+
return trusted.some((t) => {
|
|
142
|
+
try {
|
|
143
|
+
const tu = new URL(t);
|
|
144
|
+
return tu.hostname === 'localhost' || tu.hostname === '127.0.0.1' || tu.hostname === '::1';
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return trusted.some((t) => t === host || t === origin || origin.startsWith(t));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
154
|
+
const ab = Buffer.from(a);
|
|
155
|
+
const bb = Buffer.from(b);
|
|
156
|
+
if (ab.length !== bb.length) return false;
|
|
157
|
+
return ab.equals(bb);
|
|
158
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Middleware } from '../../nexus-core/src/http/index.js';
|
|
2
|
+
|
|
3
|
+
export interface SecurityHeadersOptions {
|
|
4
|
+
/** HSTS max age in seconds (0 to disable). */
|
|
5
|
+
hstsMaxAge?: number;
|
|
6
|
+
/** Include subdomains in HSTS. */
|
|
7
|
+
hstsIncludeSubdomains?: boolean;
|
|
8
|
+
/** Enable HSTS preload. */
|
|
9
|
+
hstsPreload?: boolean;
|
|
10
|
+
/** Content-Security-Policy directive string. */
|
|
11
|
+
csp?: string;
|
|
12
|
+
/** Referrer-Policy. */
|
|
13
|
+
referrerPolicy?: string;
|
|
14
|
+
/** X-Content-Type-Options. */
|
|
15
|
+
noSniff?: boolean;
|
|
16
|
+
/** X-Frame-Options (DENY/SAMEORIGIN) — superseded by CSP frame-ancestors when present. */
|
|
17
|
+
frameOptions?: 'DENY' | 'SAMEORIGIN' | false;
|
|
18
|
+
/** Cross-Origin-Opener-Policy. */
|
|
19
|
+
coop?: string;
|
|
20
|
+
/** Cross-Origin-Resource-Policy. */
|
|
21
|
+
corp?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Helmet-equivalent security headers, applied to every response. */
|
|
25
|
+
export function securityHeaders(options: SecurityHeadersOptions = {}): Middleware {
|
|
26
|
+
const opts = {
|
|
27
|
+
hstsMaxAge: options.hstsMaxAge ?? 15552000, // 180 days
|
|
28
|
+
hstsIncludeSubdomains: options.hstsIncludeSubdomains ?? true,
|
|
29
|
+
hstsPreload: options.hstsPreload ?? false,
|
|
30
|
+
csp: options.csp ?? "default-src 'self'",
|
|
31
|
+
referrerPolicy: options.referrerPolicy ?? 'no-referrer',
|
|
32
|
+
noSniff: options.noSniff ?? true,
|
|
33
|
+
frameOptions: options.frameOptions ?? 'SAMEORIGIN',
|
|
34
|
+
coop: options.coop ?? 'same-origin',
|
|
35
|
+
corp: options.corp ?? 'same-origin',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return async (ctx, next) => {
|
|
39
|
+
// Apply headers early; they persist through the response.
|
|
40
|
+
if (opts.hstsMaxAge > 0) {
|
|
41
|
+
let hsts = `max-age=${opts.hstsMaxAge}`;
|
|
42
|
+
if (opts.hstsIncludeSubdomains) hsts += '; includeSubDomains';
|
|
43
|
+
if (opts.hstsPreload) hsts += '; preload';
|
|
44
|
+
ctx.setHeader('strict-transport-security', hsts);
|
|
45
|
+
}
|
|
46
|
+
if (opts.csp) ctx.setHeader('content-security-policy', opts.csp);
|
|
47
|
+
if (opts.referrerPolicy) ctx.setHeader('referrer-policy', opts.referrerPolicy);
|
|
48
|
+
if (opts.noSniff) ctx.setHeader('x-content-type-options', 'nosniff');
|
|
49
|
+
if (opts.frameOptions) ctx.setHeader('x-frame-options', opts.frameOptions);
|
|
50
|
+
if (opts.coop) ctx.setHeader('cross-origin-opener-policy', opts.coop);
|
|
51
|
+
if (opts.corp) ctx.setHeader('cross-origin-resource-policy', opts.corp);
|
|
52
|
+
ctx.setHeader('x-xss-protection', '0'); // disabled in favor of CSP
|
|
53
|
+
await next();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './cors.js';
|
|
2
|
+
export * from './csrf.js';
|
|
3
|
+
export * from './headers.js';
|
|
4
|
+
export * from './rateLimit.js';
|
|
5
|
+
export * from './password.js';
|
|
6
|
+
export * from './jwt.js';
|
|
7
|
+
export * from './session.js';
|
|
8
|
+
export * from './rbac.js';
|
|
9
|
+
export * from './oauth.js';
|
|
10
|
+
export * from './auth.js';
|
|
11
|
+
export * from './middleware.js';
|