@nxgt/shared-hono 1.0.0
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 +15 -0
- package/dist/env.d.ts +23 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +18 -0
- package/dist/mcp/helpers.d.ts +3 -0
- package/dist/mcp/helpers.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +47 -0
- package/dist/mcp/index.js.map +11 -0
- package/dist/middlewares/accept-query.d.ts +16 -0
- package/dist/middlewares/accept-query.d.ts.map +1 -0
- package/dist/middlewares/current-user.d.ts +5 -0
- package/dist/middlewares/current-user.d.ts.map +1 -0
- package/dist/middlewares/error-handler.d.ts +9 -0
- package/dist/middlewares/error-handler.d.ts.map +1 -0
- package/dist/middlewares/index.d.ts +8 -0
- package/dist/middlewares/index.d.ts.map +1 -0
- package/dist/middlewares/openfetch-service-user.d.ts +3 -0
- package/dist/middlewares/openfetch-service-user.d.ts.map +1 -0
- package/dist/middlewares/ory-auth.d.ts +53 -0
- package/dist/middlewares/ory-auth.d.ts.map +1 -0
- package/dist/middlewares/rate-limiter.d.ts +21 -0
- package/dist/middlewares/rate-limiter.d.ts.map +1 -0
- package/dist/middlewares/secured.d.ts +18 -0
- package/dist/middlewares/secured.d.ts.map +1 -0
- package/dist/openapi-fetch.d.ts +4 -0
- package/dist/openapi-fetch.d.ts.map +1 -0
- package/dist/openapi-fetch.js +11 -0
- package/dist/openapi-fetch.js.map +10 -0
- package/dist/types/hono.d.ts +27 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/test.utils.d.ts +44 -0
- package/dist/utils/test.utils.d.ts.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @nxgt/shared-api
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run src/index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const envSchema: z.ZodObject<{
|
|
3
|
+
NODE_ENV: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
development: "development";
|
|
5
|
+
production: "production";
|
|
6
|
+
test: "test";
|
|
7
|
+
}>>;
|
|
8
|
+
PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
9
|
+
LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
|
|
10
|
+
error: "error";
|
|
11
|
+
warn: "warn";
|
|
12
|
+
info: "info";
|
|
13
|
+
debug: "debug";
|
|
14
|
+
}>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type Env = z.infer<typeof envSchema>;
|
|
17
|
+
export declare const env: {
|
|
18
|
+
NODE_ENV: "development" | "production" | "test";
|
|
19
|
+
PORT: number;
|
|
20
|
+
LOG_LEVEL: "error" | "warn" | "info" | "debug";
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,SAAS;;;;;;;;;;;;;iBAWb,CAAC;AAEH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAgB5C,eAAO,MAAM,GAAG;;;;CAGd,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAC;AAEjB,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// src/middlewares/accept-query.ts
|
|
2
|
+
import { createMiddleware } from "hono/factory";
|
|
3
|
+
var ACCEPT_QUERY_MEDIA_TYPE = "application/json";
|
|
4
|
+
function acceptQuery(mediaTypes = ACCEPT_QUERY_MEDIA_TYPE) {
|
|
5
|
+
return createMiddleware(async (ctx, next) => {
|
|
6
|
+
await next();
|
|
7
|
+
ctx.header("Accept-Query", mediaTypes);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
// src/middlewares/current-user.ts
|
|
11
|
+
import { USER_HEADERS } from "@nxgt/shared/models";
|
|
12
|
+
import { logger } from "@nxgt/shared-logging";
|
|
13
|
+
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
14
|
+
var currentUser = () => createMiddleware2(async (ctx, next) => {
|
|
15
|
+
if (!ctx.req.header(USER_HEADERS.ID) && !ctx.req.header(USER_HEADERS.CLIENT)) {
|
|
16
|
+
logger.warn("No user information found in headers");
|
|
17
|
+
return next();
|
|
18
|
+
}
|
|
19
|
+
const user = {
|
|
20
|
+
id: ctx.req.header(USER_HEADERS.ID),
|
|
21
|
+
username: ctx.req.header(USER_HEADERS.USERNAME),
|
|
22
|
+
email: ctx.req.header(USER_HEADERS.EMAIL),
|
|
23
|
+
firstName: ctx.req.header(USER_HEADERS.FIRST_NAME),
|
|
24
|
+
lastName: ctx.req.header(USER_HEADERS.LAST_NAME),
|
|
25
|
+
birthDate: ctx.req.header(USER_HEADERS.BIRTH_DATE) ? new Date(ctx.req.header(USER_HEADERS.BIRTH_DATE) || "") : null,
|
|
26
|
+
authorities: ctx.req.header(USER_HEADERS.AUTHORITIES)?.split(",") || [],
|
|
27
|
+
clientId: ctx.req.header(USER_HEADERS.CLIENT) || null,
|
|
28
|
+
roles: ctx.req.header(USER_HEADERS.ROLES)?.split(",") || [],
|
|
29
|
+
scopes: ctx.req.header(USER_HEADERS.SCOPES)?.split(",") || []
|
|
30
|
+
};
|
|
31
|
+
user.name = user.username || user.clientId || undefined;
|
|
32
|
+
logger.info(`Resolving principal : name[${user.name}] email[${user.email}] id[${user.id}] client[${user.clientId}]`);
|
|
33
|
+
ctx.set(USER_HEADERS.ID, user.id);
|
|
34
|
+
ctx.set(USER_HEADERS.USERNAME, user.username);
|
|
35
|
+
ctx.set(USER_HEADERS.EMAIL, user.email);
|
|
36
|
+
ctx.set(USER_HEADERS.NAME, user.name);
|
|
37
|
+
ctx.set(USER_HEADERS.FIRST_NAME, user.firstName);
|
|
38
|
+
ctx.set(USER_HEADERS.LAST_NAME, user.lastName);
|
|
39
|
+
ctx.set(USER_HEADERS.BIRTH_DATE, user.birthDate ? user.birthDate.toISOString() : null);
|
|
40
|
+
ctx.set(USER_HEADERS.AUTHORITIES, user.authorities);
|
|
41
|
+
ctx.set(USER_HEADERS.CLIENT, ctx.req.header(USER_HEADERS.CLIENT) || null);
|
|
42
|
+
ctx.set(USER_HEADERS.ROLES, ctx.req.header(USER_HEADERS.ROLES)?.split(",") || []);
|
|
43
|
+
ctx.set(USER_HEADERS.SCOPES, ctx.req.header(USER_HEADERS.SCOPES)?.split(",") || []);
|
|
44
|
+
ctx.set("principal", user);
|
|
45
|
+
return next();
|
|
46
|
+
});
|
|
47
|
+
// src/middlewares/error-handler.ts
|
|
48
|
+
import { CustomException } from "@nxgt/shared-exceptions";
|
|
49
|
+
import { logger as logger3 } from "@nxgt/shared-logging";
|
|
50
|
+
import { HTTPException } from "hono/http-exception";
|
|
51
|
+
|
|
52
|
+
// src/env.ts
|
|
53
|
+
import { logger as logger2 } from "@nxgt/shared-logging";
|
|
54
|
+
import { z } from "zod";
|
|
55
|
+
var envSchema = z.object({
|
|
56
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
57
|
+
PORT: z.coerce.number().default(8080),
|
|
58
|
+
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug")
|
|
59
|
+
});
|
|
60
|
+
var parseEnv = (value) => {
|
|
61
|
+
const result = envSchema.safeParse(value);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
logger2.error("❌ Invalid environment variables:");
|
|
64
|
+
logger2.error(result.error.issues);
|
|
65
|
+
throw new Error("Invalid environment variables");
|
|
66
|
+
}
|
|
67
|
+
return result.data;
|
|
68
|
+
};
|
|
69
|
+
var env = parseEnv({
|
|
70
|
+
NODE_ENV: Bun.env.NODE_ENV,
|
|
71
|
+
PORT: Bun.env.PORT
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// src/middlewares/error-handler.ts
|
|
75
|
+
var createErrorHandler = (translate, {
|
|
76
|
+
showStackInDev = true,
|
|
77
|
+
showStackInTest = false,
|
|
78
|
+
logToConsole = true
|
|
79
|
+
} = {}) => {
|
|
80
|
+
return (err, c) => {
|
|
81
|
+
if (logToConsole) {
|
|
82
|
+
logger3.error("─".repeat(60));
|
|
83
|
+
logger3.error("[Global Error]", err);
|
|
84
|
+
if (showStackInDev && env.NODE_ENV === "development" || showStackInTest && env.NODE_ENV === "test") {
|
|
85
|
+
logger3.error(err.stack);
|
|
86
|
+
}
|
|
87
|
+
logger3.error("─".repeat(60));
|
|
88
|
+
}
|
|
89
|
+
if (err instanceof CustomException) {
|
|
90
|
+
c.status(err.code);
|
|
91
|
+
return c.json({
|
|
92
|
+
status: err.code,
|
|
93
|
+
message: translate(err.message, err.options),
|
|
94
|
+
debugMessage: err.debugMessage,
|
|
95
|
+
timestamp: new Date
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof HTTPException) {
|
|
99
|
+
c.status(err.status);
|
|
100
|
+
return c.json({
|
|
101
|
+
status: err.status,
|
|
102
|
+
message: err.message,
|
|
103
|
+
debugMessage: err.stack,
|
|
104
|
+
timestamp: new Date
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
c.status(500);
|
|
108
|
+
return c.json({
|
|
109
|
+
status: 500,
|
|
110
|
+
message: translate("errors.internal-server-error"),
|
|
111
|
+
debugMessage: err.message,
|
|
112
|
+
timestamp: new Date
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
// src/middlewares/openfetch-service-user.ts
|
|
117
|
+
import { USER_HEADERS as USER_HEADERS2 } from "@nxgt/shared/models";
|
|
118
|
+
import { tryGetContext } from "hono/context-storage";
|
|
119
|
+
function openfetchServiceUser() {
|
|
120
|
+
const ctx = tryGetContext();
|
|
121
|
+
return {
|
|
122
|
+
async onRequest({ request }) {
|
|
123
|
+
if (ctx?.get(USER_HEADERS2.ID)) {
|
|
124
|
+
request.headers.set(USER_HEADERS2.ID, ctx.get(USER_HEADERS2.ID) || "");
|
|
125
|
+
request.headers.set(USER_HEADERS2.USERNAME, ctx.get(USER_HEADERS2.USERNAME) || "");
|
|
126
|
+
request.headers.set(USER_HEADERS2.EMAIL, ctx.get(USER_HEADERS2.EMAIL) || "");
|
|
127
|
+
request.headers.set(USER_HEADERS2.FIRST_NAME, ctx.get(USER_HEADERS2.FIRST_NAME) || "");
|
|
128
|
+
request.headers.set(USER_HEADERS2.LAST_NAME, ctx.get(USER_HEADERS2.LAST_NAME) || "");
|
|
129
|
+
request.headers.set(USER_HEADERS2.BIRTH_DATE, ctx.get(USER_HEADERS2.BIRTH_DATE) || "");
|
|
130
|
+
request.headers.set(USER_HEADERS2.AUTHORITIES, ctx.get(USER_HEADERS2.AUTHORITIES)?.join(",") || "");
|
|
131
|
+
request.headers.set(USER_HEADERS2.CLIENT, ctx.get(USER_HEADERS2.CLIENT) || "");
|
|
132
|
+
request.headers.set(USER_HEADERS2.ROLES, ctx.get(USER_HEADERS2.ROLES)?.join(",") || "");
|
|
133
|
+
request.headers.set(USER_HEADERS2.SCOPES, ctx.get(USER_HEADERS2.SCOPES)?.join(",") || "");
|
|
134
|
+
request.headers.set("X-Service", "gateway");
|
|
135
|
+
}
|
|
136
|
+
return request;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// src/middlewares/ory-auth.ts
|
|
141
|
+
import { USER_HEADERS as USER_HEADERS4 } from "@nxgt/shared/models";
|
|
142
|
+
import { CustomException as CustomException2 } from "@nxgt/shared-exceptions";
|
|
143
|
+
import { logger as logger4 } from "@nxgt/shared-logging";
|
|
144
|
+
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
145
|
+
import {
|
|
146
|
+
bearerOf,
|
|
147
|
+
OryUnavailable
|
|
148
|
+
} from "stx-sdk/ory";
|
|
149
|
+
|
|
150
|
+
// src/utils/test.utils.ts
|
|
151
|
+
import { USER_HEADERS as USER_HEADERS3 } from "@nxgt/shared/models";
|
|
152
|
+
import { mongoose } from "@nxgt/shared-mongo";
|
|
153
|
+
function principalFromMockHeaders(ctx) {
|
|
154
|
+
if (!ctx.req.header(USER_HEADERS3.ID) && !ctx.req.header(USER_HEADERS3.CLIENT)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const principal = {
|
|
158
|
+
id: ctx.req.header(USER_HEADERS3.ID),
|
|
159
|
+
username: ctx.req.header(USER_HEADERS3.USERNAME),
|
|
160
|
+
email: ctx.req.header(USER_HEADERS3.EMAIL),
|
|
161
|
+
firstName: ctx.req.header(USER_HEADERS3.FIRST_NAME),
|
|
162
|
+
lastName: ctx.req.header(USER_HEADERS3.LAST_NAME),
|
|
163
|
+
birthDate: ctx.req.header(USER_HEADERS3.BIRTH_DATE) ? new Date(ctx.req.header(USER_HEADERS3.BIRTH_DATE) || "") : null,
|
|
164
|
+
authorities: ctx.req.header(USER_HEADERS3.AUTHORITIES)?.split(",") || [],
|
|
165
|
+
clientId: ctx.req.header(USER_HEADERS3.CLIENT) || null,
|
|
166
|
+
roles: ctx.req.header(USER_HEADERS3.ROLES)?.split(",") || [],
|
|
167
|
+
scopes: ctx.req.header(USER_HEADERS3.SCOPES)?.split(",") || []
|
|
168
|
+
};
|
|
169
|
+
principal.name = principal.username || principal.clientId || undefined;
|
|
170
|
+
return principal;
|
|
171
|
+
}
|
|
172
|
+
function mockUser(values) {
|
|
173
|
+
return {
|
|
174
|
+
id: new mongoose.mongo.ObjectId().toHexString(),
|
|
175
|
+
authorities: [...values.roles ?? [], ...values.permissions ?? []],
|
|
176
|
+
name: values.username || undefined,
|
|
177
|
+
...values
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function mockAuthMiddleware(user) {
|
|
181
|
+
return {
|
|
182
|
+
async onRequest({ request }) {
|
|
183
|
+
request.headers.set(USER_HEADERS3.ID, user.id || "");
|
|
184
|
+
request.headers.set(USER_HEADERS3.USERNAME, user.username || "");
|
|
185
|
+
request.headers.set(USER_HEADERS3.EMAIL, user.email || "");
|
|
186
|
+
request.headers.set(USER_HEADERS3.AUTHORITIES, user.authorities?.join(",") || "");
|
|
187
|
+
request.headers.set(USER_HEADERS3.BIRTH_DATE, user.birthDate?.toISOString() ?? "");
|
|
188
|
+
request.headers.set(USER_HEADERS3.FIRST_NAME, user.firstName ?? "");
|
|
189
|
+
request.headers.set(USER_HEADERS3.LAST_NAME, user.lastName ?? "");
|
|
190
|
+
return request;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
var asQueryMethod = {
|
|
195
|
+
onRequest: async ({ request }) => {
|
|
196
|
+
const url = new URL(request.url);
|
|
197
|
+
url.pathname = url.pathname.replace(/\/search$/, "");
|
|
198
|
+
const body = await request.text();
|
|
199
|
+
return new Request(url, {
|
|
200
|
+
method: "QUERY",
|
|
201
|
+
headers: request.headers,
|
|
202
|
+
body: body || undefined
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/middlewares/ory-auth.ts
|
|
208
|
+
function oryAuth(ory) {
|
|
209
|
+
return createMiddleware3(async (ctx, next) => {
|
|
210
|
+
if (env.NODE_ENV === "test") {
|
|
211
|
+
const mockPrincipal = principalFromMockHeaders(ctx);
|
|
212
|
+
if (mockPrincipal) {
|
|
213
|
+
ctx.set("principal", mockPrincipal);
|
|
214
|
+
ctx.set("ory", oryPrincipalFromMock(mockPrincipal));
|
|
215
|
+
ctx.set(USER_HEADERS4.CLAIMS, {
|
|
216
|
+
sub: mockPrincipal.id,
|
|
217
|
+
username: mockPrincipal.username ?? undefined,
|
|
218
|
+
clientId: mockPrincipal.clientId ?? undefined,
|
|
219
|
+
authorities: mockPrincipal.authorities ?? [],
|
|
220
|
+
roles: mockPrincipal.roles ?? [],
|
|
221
|
+
scope: mockPrincipal.scopes?.join(" ")
|
|
222
|
+
});
|
|
223
|
+
return next();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let resolved;
|
|
227
|
+
try {
|
|
228
|
+
resolved = await ory.resolve(ctx.req.raw.headers);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (!(error instanceof OryUnavailable))
|
|
231
|
+
throw error;
|
|
232
|
+
throw serviceUnavailable(error);
|
|
233
|
+
}
|
|
234
|
+
if (!resolved) {
|
|
235
|
+
ctx.set("ory", null);
|
|
236
|
+
return next();
|
|
237
|
+
}
|
|
238
|
+
const principal = toPrincipal(resolved);
|
|
239
|
+
logger4.info(`Resolving principal (ory ${resolved.kind}): subject[${resolved.subject}]`);
|
|
240
|
+
ctx.set("principal", principal);
|
|
241
|
+
ctx.set("ory", resolved);
|
|
242
|
+
ctx.set("accessToken", bearerOf(ctx.req.raw.headers));
|
|
243
|
+
ctx.set(USER_HEADERS4.CLAIMS, {
|
|
244
|
+
sub: resolved.subject,
|
|
245
|
+
kind: resolved.kind,
|
|
246
|
+
email: resolved.identity?.email,
|
|
247
|
+
email_verified: resolved.identity?.verified,
|
|
248
|
+
clientId: resolved.clientId,
|
|
249
|
+
scope: resolved.scopes.join(" ") || undefined,
|
|
250
|
+
aud: resolved.audience,
|
|
251
|
+
aal: resolved.aal,
|
|
252
|
+
exp: resolved.expiresAt?.toISOString()
|
|
253
|
+
});
|
|
254
|
+
return next();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function serviceUnavailable(error) {
|
|
258
|
+
logger4.error(`Ory ${error.service} unavailable (${error.status}): ${JSON.stringify(error.body)}`);
|
|
259
|
+
return CustomException2.from({
|
|
260
|
+
message: "errors.service-unavailable",
|
|
261
|
+
code: 503,
|
|
262
|
+
debugMessage: `ory: ${error.message}`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function withOryUnavailable(handler) {
|
|
266
|
+
return (error, ctx) => handler(error instanceof OryUnavailable ? serviceUnavailable(error) : error, ctx);
|
|
267
|
+
}
|
|
268
|
+
function toPrincipal(ory) {
|
|
269
|
+
const principal = {
|
|
270
|
+
id: ory.subject,
|
|
271
|
+
username: ory.identity?.email ?? null,
|
|
272
|
+
email: ory.identity?.email ?? null,
|
|
273
|
+
firstName: ory.identity?.name?.first ?? null,
|
|
274
|
+
lastName: ory.identity?.name?.last ?? null,
|
|
275
|
+
birthDate: null,
|
|
276
|
+
authorities: [],
|
|
277
|
+
roles: [],
|
|
278
|
+
clientId: ory.clientId ?? null,
|
|
279
|
+
scopes: ory.scopes
|
|
280
|
+
};
|
|
281
|
+
principal.name = principal.username || principal.clientId || undefined;
|
|
282
|
+
return principal;
|
|
283
|
+
}
|
|
284
|
+
function oryPrincipalFromMock(mock) {
|
|
285
|
+
return {
|
|
286
|
+
subject: mock.id ?? mock.clientId ?? "mock",
|
|
287
|
+
kind: mock.clientId && !mock.id ? "token" : "session",
|
|
288
|
+
identity: mock.email ? {
|
|
289
|
+
email: mock.email,
|
|
290
|
+
name: {
|
|
291
|
+
first: mock.firstName ?? undefined,
|
|
292
|
+
last: mock.lastName ?? undefined
|
|
293
|
+
},
|
|
294
|
+
verified: true
|
|
295
|
+
} : undefined,
|
|
296
|
+
scopes: mock.scopes ?? [],
|
|
297
|
+
clientId: mock.clientId ?? undefined
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// src/middlewares/rate-limiter.ts
|
|
301
|
+
import { logger as logger5 } from "@nxgt/shared-logging";
|
|
302
|
+
import { Redis as UpstashRedis } from "@upstash/redis";
|
|
303
|
+
import {
|
|
304
|
+
rateLimiter as honoRateLimiter,
|
|
305
|
+
RedisStore
|
|
306
|
+
} from "hono-rate-limiter";
|
|
307
|
+
import IORedis from "ioredis";
|
|
308
|
+
function ioredisAdapter(client) {
|
|
309
|
+
return {
|
|
310
|
+
scriptLoad: (script) => client.script("LOAD", script),
|
|
311
|
+
evalsha: (sha1, keys, args) => client.evalsha(sha1, keys.length, ...keys, ...args),
|
|
312
|
+
decr: (key) => client.decr(key),
|
|
313
|
+
del: (key) => client.del(key)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function redisClientFor(redisUrl, redisToken) {
|
|
317
|
+
if (redisUrl.startsWith("redis://") || redisUrl.startsWith("rediss://")) {
|
|
318
|
+
const client = new IORedis(redisUrl);
|
|
319
|
+
client.on("error", (err) => {
|
|
320
|
+
logger5.error(`[rate-limiter] Redis connection error: ${err.message}`);
|
|
321
|
+
});
|
|
322
|
+
return ioredisAdapter(client);
|
|
323
|
+
}
|
|
324
|
+
return new UpstashRedis({
|
|
325
|
+
url: redisUrl,
|
|
326
|
+
token: redisToken
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function rateLimiter(options) {
|
|
330
|
+
return honoRateLimiter({
|
|
331
|
+
...options,
|
|
332
|
+
windowMs: options?.windowMs ?? 1 * 60 * 1000,
|
|
333
|
+
limit: options?.limit ?? 10,
|
|
334
|
+
keyGenerator: options?.keyGenerator ?? ((c) => c.req.header("x-forwarded-for") ?? ""),
|
|
335
|
+
store: options?.redisUrl ? new RedisStore({
|
|
336
|
+
client: redisClientFor(options.redisUrl, options.redisToken),
|
|
337
|
+
prefix: options.prefix
|
|
338
|
+
}) : undefined
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// src/middlewares/secured.ts
|
|
342
|
+
import { isScopeAuthority } from "@nxgt/shared/helpers";
|
|
343
|
+
import { CustomException as CustomException3 } from "@nxgt/shared-exceptions";
|
|
344
|
+
import { getLogger } from "@nxgt/shared-logging";
|
|
345
|
+
import { createMiddleware as createMiddleware4 } from "hono/factory";
|
|
346
|
+
function secured(authorities = []) {
|
|
347
|
+
return createMiddleware4(async (ctx, next) => {
|
|
348
|
+
const logger6 = ctx.get("logger") || getLogger();
|
|
349
|
+
logger6.info("Secured middleware: checking authorities");
|
|
350
|
+
const user = ctx.get("principal");
|
|
351
|
+
if (!user) {
|
|
352
|
+
logger6.error("Unauthenticated access attempt");
|
|
353
|
+
throw CustomException3.unauthorized({
|
|
354
|
+
message: "errors.unauthenticated"
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (user.roles?.includes("ADMIN")) {
|
|
358
|
+
return next();
|
|
359
|
+
}
|
|
360
|
+
logger6.info(`User authenticated: ${user.name ?? user.username ?? user.clientId}`);
|
|
361
|
+
if (!authorities.length) {
|
|
362
|
+
return next();
|
|
363
|
+
}
|
|
364
|
+
const isClient = !!user.clientId && !user.username;
|
|
365
|
+
const userAuthorities = user.authorities ?? [];
|
|
366
|
+
const effectiveUserAuthorities = isClient ? userAuthorities.filter(isScopeAuthority) : userAuthorities;
|
|
367
|
+
const granted = authorities.every((group) => {
|
|
368
|
+
if (group.length === 0)
|
|
369
|
+
return true;
|
|
370
|
+
return group.some((authority) => effectiveUserAuthorities.includes(authority));
|
|
371
|
+
});
|
|
372
|
+
if (granted) {
|
|
373
|
+
await next();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
throw CustomException3.forbidden({
|
|
377
|
+
message: "errors.forbidden"
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
ACCEPT_QUERY_MEDIA_TYPE,
|
|
383
|
+
USER_HEADERS,
|
|
384
|
+
acceptQuery,
|
|
385
|
+
asQueryMethod,
|
|
386
|
+
createErrorHandler,
|
|
387
|
+
currentUser,
|
|
388
|
+
mockAuthMiddleware,
|
|
389
|
+
mockUser,
|
|
390
|
+
openfetchServiceUser,
|
|
391
|
+
oryAuth,
|
|
392
|
+
principalFromMockHeaders,
|
|
393
|
+
rateLimiter,
|
|
394
|
+
secured,
|
|
395
|
+
serviceUnavailable,
|
|
396
|
+
toPrincipal,
|
|
397
|
+
withOryUnavailable
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
//# debugId=EC87DAEAD6B394CA64756E2164756E21
|
|
401
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/middlewares/accept-query.ts", "../src/middlewares/current-user.ts", "../src/middlewares/error-handler.ts", "../src/env.ts", "../src/middlewares/openfetch-service-user.ts", "../src/middlewares/ory-auth.ts", "../src/utils/test.utils.ts", "../src/middlewares/rate-limiter.ts", "../src/middlewares/secured.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createMiddleware } from 'hono/factory';\n\n/** Media types accepted in a QUERY request body, advertised by `acceptQuery()`. */\nexport const ACCEPT_QUERY_MEDIA_TYPE = 'application/json';\n\n/**\n * Advertises QUERY support on a resource.\n *\n * `Accept-Query` is how the safe-method-with-body draft says a resource\n * announces both that it answers QUERY *and* which media types it will accept\n * in the request content. It rides on the QUERY registration only: the\n * `POST …/search` route it shares its handlers with is left exactly as it was,\n * headers included, so that every existing caller sees no change at all.\n *\n * Set after `next()` so it lands on whatever the handler produced, errors\n * included.\n */\nexport function acceptQuery(mediaTypes: string = ACCEPT_QUERY_MEDIA_TYPE) {\n\treturn createMiddleware(async (ctx, next) => {\n\t\tawait next();\n\t\tctx.header('Accept-Query', mediaTypes);\n\t});\n}\n",
|
|
6
|
+
"import { type Principal, USER_HEADERS } from '@nxgt/shared/models';\nimport { logger } from '@nxgt/shared-logging';\nimport { createMiddleware } from 'hono/factory';\nimport type { MiddlewareHandler } from 'hono/types';\n\nexport const currentUser = (): MiddlewareHandler =>\n\tcreateMiddleware(async (ctx, next) => {\n\t\tif (\n\t\t\t!ctx.req.header(USER_HEADERS.ID) &&\n\t\t\t!ctx.req.header(USER_HEADERS.CLIENT)\n\t\t) {\n\t\t\tlogger.warn('No user information found in headers');\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user: Principal = {\n\t\t\tid: ctx.req.header(USER_HEADERS.ID),\n\t\t\tusername: ctx.req.header(USER_HEADERS.USERNAME),\n\t\t\temail: ctx.req.header(USER_HEADERS.EMAIL),\n\t\t\tfirstName: ctx.req.header(USER_HEADERS.FIRST_NAME),\n\t\t\tlastName: ctx.req.header(USER_HEADERS.LAST_NAME),\n\t\t\tbirthDate: ctx.req.header(USER_HEADERS.BIRTH_DATE)\n\t\t\t\t? new Date(ctx.req.header(USER_HEADERS.BIRTH_DATE) || '')\n\t\t\t\t: null,\n\t\t\tauthorities: ctx.req.header(USER_HEADERS.AUTHORITIES)?.split(',') || [],\n\t\t\tclientId: ctx.req.header(USER_HEADERS.CLIENT) || null,\n\t\t\troles: ctx.req.header(USER_HEADERS.ROLES)?.split(',') || [],\n\t\t\tscopes: ctx.req.header(USER_HEADERS.SCOPES)?.split(',') || [],\n\t\t};\n\n\t\tuser.name = user.username || user.clientId || undefined;\n\n\t\tlogger.info(\n\t\t\t`Resolving principal : name[${user.name}] email[${user.email}] id[${user.id}] client[${user.clientId}]`,\n\t\t);\n\n\t\tctx.set(USER_HEADERS.ID, user.id);\n\t\tctx.set(USER_HEADERS.USERNAME, user.username);\n\t\tctx.set(USER_HEADERS.EMAIL, user.email);\n\t\tctx.set(USER_HEADERS.NAME, user.name);\n\t\tctx.set(USER_HEADERS.FIRST_NAME, user.firstName);\n\t\tctx.set(USER_HEADERS.LAST_NAME, user.lastName);\n\t\tctx.set(\n\t\t\tUSER_HEADERS.BIRTH_DATE,\n\t\t\tuser.birthDate ? user.birthDate.toISOString() : null,\n\t\t);\n\t\tctx.set(USER_HEADERS.AUTHORITIES, user.authorities);\n\t\tctx.set(USER_HEADERS.CLIENT, ctx.req.header(USER_HEADERS.CLIENT) || null);\n\t\tctx.set(\n\t\t\tUSER_HEADERS.ROLES,\n\t\t\tctx.req.header(USER_HEADERS.ROLES)?.split(',') || [],\n\t\t);\n\t\tctx.set(\n\t\t\tUSER_HEADERS.SCOPES,\n\t\t\tctx.req.header(USER_HEADERS.SCOPES)?.split(',') || [],\n\t\t);\n\n\t\tctx.set('principal', user);\n\n\t\treturn next();\n\t});\n\nexport { type Principal, USER_HEADERS };\n",
|
|
7
|
+
"import type { LocaleKey } from '@nxgt/i18n';\nimport { CustomException } from '@nxgt/shared-exceptions';\nimport { logger } from '@nxgt/shared-logging';\nimport type { ErrorHandler } from 'hono';\nimport { HTTPException } from 'hono/http-exception';\nimport { env } from '../env';\n\nexport type ErrorHandlerOptions = {\n\tshowStackInDev?: boolean;\n\tshowStackInTest?: boolean;\n\tlogToConsole?: boolean;\n};\n\nexport const createErrorHandler = <K extends LocaleKey = LocaleKey>(\n\ttranslate: (key: K, context?: Record<string, any>) => string,\n\t{\n\t\tshowStackInDev = true,\n\t\tshowStackInTest = false,\n\t\tlogToConsole = true,\n\t}: ErrorHandlerOptions = {},\n): ErrorHandler => {\n\treturn (err, c) => {\n\t\tif (logToConsole) {\n\t\t\tlogger.error('─'.repeat(60));\n\t\t\tlogger.error('[Global Error]', err);\n\t\t\tif (\n\t\t\t\t(showStackInDev && env.NODE_ENV === 'development') ||\n\t\t\t\t(showStackInTest && env.NODE_ENV === 'test')\n\t\t\t) {\n\t\t\t\tlogger.error(err.stack);\n\t\t\t}\n\t\t\tlogger.error('─'.repeat(60));\n\t\t}\n\n\t\tif (err instanceof CustomException) {\n\t\t\tc.status(err.code);\n\t\t\treturn c.json({\n\t\t\t\tstatus: err.code,\n\t\t\t\tmessage: translate(err.message as K, err.options),\n\t\t\t\tdebugMessage: err.debugMessage,\n\t\t\t\ttimestamp: new Date(),\n\t\t\t});\n\t\t}\n\t\tif (err instanceof HTTPException) {\n\t\t\tc.status(err.status);\n\t\t\treturn c.json({\n\t\t\t\tstatus: err.status,\n\t\t\t\tmessage: err.message,\n\t\t\t\tdebugMessage: err.stack,\n\t\t\t\ttimestamp: new Date(),\n\t\t\t});\n\t\t}\n\t\tc.status(500);\n\t\treturn c.json({\n\t\t\tstatus: 500,\n\t\t\tmessage: translate('errors.internal-server-error' as K),\n\t\t\tdebugMessage: err.message,\n\t\t\ttimestamp: new Date(),\n\t\t});\n\t};\n};\n",
|
|
8
|
+
"import { logger } from '@nxgt/shared-logging';\nimport { z } from 'zod';\n\n// Define schema\nconst envSchema = z.object({\n\t// Node environment\n\tNODE_ENV: z\n\t\t.enum(['development', 'production', 'test'])\n\t\t.default('development'),\n\n\t// Server\n\tPORT: z.coerce.number().default(8080),\n\n\t// Logging\n\tLOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('debug'),\n});\n\nexport type Env = z.infer<typeof envSchema>;\n\n// Parse and validate environment variables\nconst parseEnv = (value: Record<string, unknown>): Env => {\n\tconst result = envSchema.safeParse(value);\n\n\tif (!result.success) {\n\t\tlogger.error('❌ Invalid environment variables:');\n\t\tlogger.error(result.error.issues);\n\t\tthrow new Error('Invalid environment variables');\n\t}\n\n\treturn result.data;\n};\n\n// Export validated and typed environment variables\nexport const env = parseEnv({\n\tNODE_ENV: Bun.env.NODE_ENV,\n\tPORT: Bun.env.PORT,\n});\n",
|
|
9
|
+
"import { USER_HEADERS } from '@nxgt/shared/models';\nimport { tryGetContext } from 'hono/context-storage';\nimport type { Middleware } from 'openapi-fetch';\n\nexport function openfetchServiceUser(): Middleware {\n\tconst ctx = tryGetContext();\n\treturn {\n\t\tasync onRequest({ request }) {\n\t\t\tif (ctx?.get(USER_HEADERS.ID)) {\n\t\t\t\trequest.headers.set(USER_HEADERS.ID, ctx.get(USER_HEADERS.ID) || '');\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.USERNAME,\n\t\t\t\t\tctx.get(USER_HEADERS.USERNAME) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.EMAIL,\n\t\t\t\t\tctx.get(USER_HEADERS.EMAIL) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.FIRST_NAME,\n\t\t\t\t\tctx.get(USER_HEADERS.FIRST_NAME) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.LAST_NAME,\n\t\t\t\t\tctx.get(USER_HEADERS.LAST_NAME) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.BIRTH_DATE,\n\t\t\t\t\tctx.get(USER_HEADERS.BIRTH_DATE) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.AUTHORITIES,\n\t\t\t\t\tctx.get(USER_HEADERS.AUTHORITIES)?.join(',') || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.CLIENT,\n\t\t\t\t\tctx.get(USER_HEADERS.CLIENT) || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.ROLES,\n\t\t\t\t\tctx.get(USER_HEADERS.ROLES)?.join(',') || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set(\n\t\t\t\t\tUSER_HEADERS.SCOPES,\n\t\t\t\t\tctx.get(USER_HEADERS.SCOPES)?.join(',') || '',\n\t\t\t\t);\n\t\t\t\trequest.headers.set('X-Service', 'gateway');\n\t\t\t}\n\t\t\treturn request;\n\t\t},\n\t};\n}\n",
|
|
10
|
+
"import { type Principal, USER_HEADERS } from '@nxgt/shared/models';\nimport { CustomException } from '@nxgt/shared-exceptions';\nimport { logger } from '@nxgt/shared-logging';\nimport type { ErrorHandler } from 'hono';\nimport { createMiddleware } from 'hono/factory';\nimport {\n\tbearerOf,\n\ttype Ory,\n\ttype OryPrincipal,\n\tOryUnavailable,\n} from 'stx-sdk/ory';\nimport { env } from '../env';\nimport { principalFromMockHeaders } from '../utils/test.utils';\n\n/**\n * Authentication for an Ory-native API — the twin of storex-api's\n * `remoteAuth()`, with the Ory stack instead of oauth-api as the authority.\n * Same contract, so `policyGuard` and a `rules.yaml` keep answering 401 for\n * `authenticated: true` without knowing which authority signed the caller in:\n *\n * - `NODE_ENV=test` and `X-User-*` headers present ⇒ the mock principal, as\n * every route spec in this repo expects. The Ory principal is synthesised\n * from it so `<module>.access.ts` sees a `subject` either way.\n * - no credential, or one Kratos / Hydra does not honour ⇒ `next()` as an\n * anonymous caller. The rules file decides whether that is a 401.\n * - Kratos, Hydra or Keto unreachable ⇒ **503**, fail closed. Never an\n * anonymous `next()`: that turns an outage into a lockout with no error.\n * - otherwise `principal` (the repo-wide shape), `ory` (the Ory one —\n * `subject` is what Keto receives), `accessToken` (the Bearer, if that is\n * what came in) and `X-Claims`.\n *\n * Takes an `Ory` rather than URLs so the app builds one `createOry()` from\n * its own zod-validated env and shares it with its access layer.\n */\nexport function oryAuth(ory: Ory) {\n\treturn createMiddleware(async (ctx, next) => {\n\t\tif (env.NODE_ENV === 'test') {\n\t\t\tconst mockPrincipal = principalFromMockHeaders(ctx);\n\t\t\tif (mockPrincipal) {\n\t\t\t\tctx.set('principal', mockPrincipal);\n\t\t\t\tctx.set('ory', oryPrincipalFromMock(mockPrincipal));\n\t\t\t\tctx.set(USER_HEADERS.CLAIMS, {\n\t\t\t\t\tsub: mockPrincipal.id,\n\t\t\t\t\tusername: mockPrincipal.username ?? undefined,\n\t\t\t\t\tclientId: mockPrincipal.clientId ?? undefined,\n\t\t\t\t\tauthorities: mockPrincipal.authorities ?? [],\n\t\t\t\t\troles: mockPrincipal.roles ?? [],\n\t\t\t\t\tscope: mockPrincipal.scopes?.join(' '),\n\t\t\t\t});\n\t\t\t\treturn next();\n\t\t\t}\n\t\t}\n\n\t\tlet resolved: OryPrincipal | null;\n\t\ttry {\n\t\t\tresolved = await ory.resolve(ctx.req.raw.headers);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof OryUnavailable)) throw error;\n\t\t\tthrow serviceUnavailable(error);\n\t\t}\n\n\t\tif (!resolved) {\n\t\t\tctx.set('ory', null);\n\t\t\treturn next();\n\t\t}\n\n\t\tconst principal = toPrincipal(resolved);\n\t\tlogger.info(\n\t\t\t`Resolving principal (ory ${resolved.kind}): subject[${resolved.subject}]`,\n\t\t);\n\n\t\tctx.set('principal', principal);\n\t\tctx.set('ory', resolved);\n\t\tctx.set('accessToken', bearerOf(ctx.req.raw.headers));\n\t\tctx.set(USER_HEADERS.CLAIMS, {\n\t\t\tsub: resolved.subject,\n\t\t\tkind: resolved.kind,\n\t\t\temail: resolved.identity?.email,\n\t\t\temail_verified: resolved.identity?.verified,\n\t\t\tclientId: resolved.clientId,\n\t\t\tscope: resolved.scopes.join(' ') || undefined,\n\t\t\taud: resolved.audience,\n\t\t\taal: resolved.aal,\n\t\t\texp: resolved.expiresAt?.toISOString(),\n\t\t});\n\n\t\treturn next();\n\t});\n}\n\n/**\n * The 503 an `OryUnavailable` becomes — for the middleware above and for\n * `withOryUnavailable` below, so a Keto outage in a service's `isAllowed`\n * answers the same thing as a Kratos outage in `resolve`.\n */\nexport function serviceUnavailable(error: OryUnavailable): CustomException {\n\tlogger.error(\n\t\t`Ory ${error.service} unavailable (${error.status}): ${JSON.stringify(error.body)}`,\n\t);\n\treturn CustomException.from({\n\t\tmessage: 'errors.service-unavailable',\n\t\tcode: 503,\n\t\tdebugMessage: `ory: ${error.message}`,\n\t});\n}\n\n/**\n * Wraps the app's error handler so an `OryUnavailable` thrown anywhere\n * below the middleware — an access layer asking Keto, a service listing\n * tuples — is a 503 and not the generic 500. Without it the middleware's\n * own mapping only covers `resolve`, and a Keto restart would surface as\n * \"Internal server error\" from every guarded route:\n *\n * ```ts\n * app.onError(withOryUnavailable(createErrorHandler(translate)));\n * ```\n */\nexport function withOryUnavailable(handler: ErrorHandler): ErrorHandler {\n\treturn (error, ctx) =>\n\t\thandler(\n\t\t\terror instanceof OryUnavailable ? serviceUnavailable(error) : error,\n\t\t\tctx,\n\t\t);\n}\n\n/**\n * The repo-wide `Principal` from an Ory one. `id` is the subject — the\n * identity id or the client id — because `id` is what every existing\n * `ownerId` comparison reads, and a tuple written for `subject` must match\n * an ownership check written for `id`. No authorities and no roles: Keto\n * answers those questions per object, and an empty list is what stops a\n * `@policy`-style check from granting anything by accident.\n */\nexport function toPrincipal(ory: OryPrincipal): Principal {\n\tconst principal: Principal = {\n\t\tid: ory.subject,\n\t\tusername: ory.identity?.email ?? null,\n\t\temail: ory.identity?.email ?? null,\n\t\tfirstName: ory.identity?.name?.first ?? null,\n\t\tlastName: ory.identity?.name?.last ?? null,\n\t\tbirthDate: null,\n\t\tauthorities: [],\n\t\troles: [],\n\t\tclientId: ory.clientId ?? null,\n\t\tscopes: ory.scopes,\n\t};\n\tprincipal.name = principal.username || principal.clientId || undefined;\n\treturn principal;\n}\n\n/** What a route spec's `mockUser()` looks like once it has been through Ory. */\nfunction oryPrincipalFromMock(mock: Principal): OryPrincipal {\n\treturn {\n\t\tsubject: mock.id ?? mock.clientId ?? 'mock',\n\t\tkind: mock.clientId && !mock.id ? 'token' : 'session',\n\t\tidentity: mock.email\n\t\t\t? {\n\t\t\t\t\temail: mock.email,\n\t\t\t\t\tname: {\n\t\t\t\t\t\tfirst: mock.firstName ?? undefined,\n\t\t\t\t\t\tlast: mock.lastName ?? undefined,\n\t\t\t\t\t},\n\t\t\t\t\tverified: true,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tscopes: mock.scopes ?? [],\n\t\tclientId: mock.clientId ?? undefined,\n\t};\n}\n",
|
|
11
|
+
"import { type Principal, USER_HEADERS } from '@nxgt/shared/models';\nimport { mongoose } from '@nxgt/shared-mongo';\nimport type { Context } from 'hono';\nimport type { Middleware } from 'openapi-fetch';\n\n/**\n * Inverse of `mockAuthMiddleware`: reads the `X-User-*` headers it injects\n * back into a `Principal`. For services that verify tokens themselves\n * (zero-trust, no gateway in front) but still want their existing route\n * specs — written against `mockAuthMiddleware` — to work unchanged in\n * NODE_ENV=test. Returns `undefined` when no mock headers are present, so\n * callers can fall back to real token verification.\n */\nexport function principalFromMockHeaders(ctx: Context): Principal | undefined {\n\tif (\n\t\t!ctx.req.header(USER_HEADERS.ID) &&\n\t\t!ctx.req.header(USER_HEADERS.CLIENT)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst principal: Principal = {\n\t\tid: ctx.req.header(USER_HEADERS.ID),\n\t\tusername: ctx.req.header(USER_HEADERS.USERNAME),\n\t\temail: ctx.req.header(USER_HEADERS.EMAIL),\n\t\tfirstName: ctx.req.header(USER_HEADERS.FIRST_NAME),\n\t\tlastName: ctx.req.header(USER_HEADERS.LAST_NAME),\n\t\tbirthDate: ctx.req.header(USER_HEADERS.BIRTH_DATE)\n\t\t\t? new Date(ctx.req.header(USER_HEADERS.BIRTH_DATE) || '')\n\t\t\t: null,\n\t\tauthorities: ctx.req.header(USER_HEADERS.AUTHORITIES)?.split(',') || [],\n\t\tclientId: ctx.req.header(USER_HEADERS.CLIENT) || null,\n\t\troles: ctx.req.header(USER_HEADERS.ROLES)?.split(',') || [],\n\t\tscopes: ctx.req.header(USER_HEADERS.SCOPES)?.split(',') || [],\n\t};\n\tprincipal.name = principal.username || principal.clientId || undefined;\n\treturn principal;\n}\n\nexport function mockUser(\n\tvalues: Omit<Principal, 'authorities' | 'id'> & {\n\t\tpermissions?: string[];\n\t\troles?: string[];\n\t},\n): Principal {\n\treturn {\n\t\tid: new mongoose.mongo.ObjectId().toHexString(),\n\t\tauthorities: [...(values.roles ?? []), ...(values.permissions ?? [])],\n\t\tname: values.username || undefined,\n\t\t...values,\n\t};\n}\n\nexport function mockAuthMiddleware(user: Principal): Middleware {\n\treturn {\n\t\tasync onRequest({ request }) {\n\t\t\trequest.headers.set(USER_HEADERS.ID, user.id || '');\n\t\t\trequest.headers.set(USER_HEADERS.USERNAME, user.username || '');\n\t\t\trequest.headers.set(USER_HEADERS.EMAIL, user.email || '');\n\t\t\trequest.headers.set(\n\t\t\t\tUSER_HEADERS.AUTHORITIES,\n\t\t\t\tuser.authorities?.join(',') || '',\n\t\t\t);\n\t\t\trequest.headers.set(\n\t\t\t\tUSER_HEADERS.BIRTH_DATE,\n\t\t\t\tuser.birthDate?.toISOString() ?? '',\n\t\t\t);\n\t\t\trequest.headers.set(USER_HEADERS.FIRST_NAME, user.firstName ?? '');\n\t\t\trequest.headers.set(USER_HEADERS.LAST_NAME, user.lastName ?? '');\n\t\t\treturn request;\n\t\t},\n\t};\n}\n\n/**\n * openapi-fetch middleware that turns a `POST <resource>/search` call into the\n * `QUERY <resource>` it mirrors — same body, same response, safe method.\n *\n * The generated client cannot express QUERY: `openapi-typescript` has no\n * `query` path-item key (its method list is the eight classic verbs), so the\n * operation is invisible to the `paths` type, and `openapi-fetch`'s own\n * `request()` is constrained to `HttpMethod`. Call the POST search operation\n * for the types — request and response are identical either way — and let this\n * rewrite the verb *and* drop the `/search` suffix on the wire:\n *\n * ```ts\n * client.use(mockAuthMiddleware(principal));\n * const { data, error } = await client.POST('/tags/search', {\n * body: {},\n * middleware: [asQueryMethod],\n * }); // actually sends: QUERY /tags\n * ```\n *\n * Per-request middleware runs after the ones registered with `client.use()`,\n * so headers set by `mockAuthMiddleware` are already on the request and are\n * carried over. The body is read to a string rather than passed as a stream:\n * a streaming body would need `duplex: 'half'`, and these are small JSON\n * payloads.\n */\nexport const asQueryMethod: Middleware = {\n\tonRequest: async ({ request }) => {\n\t\tconst url = new URL(request.url);\n\t\turl.pathname = url.pathname.replace(/\\/search$/, '');\n\t\tconst body = await request.text();\n\t\treturn new Request(url, {\n\t\t\tmethod: 'QUERY',\n\t\t\theaders: request.headers,\n\t\t\tbody: body || undefined,\n\t\t});\n\t},\n};\n",
|
|
12
|
+
"import { logger } from '@nxgt/shared-logging';\nimport { Redis as UpstashRedis } from '@upstash/redis';\nimport {\n\ttype HonoConfigProps,\n\trateLimiter as honoRateLimiter,\n\ttype RedisClient,\n\tRedisStore,\n} from 'hono-rate-limiter';\nimport IORedis from 'ioredis';\n\nexport type RateLimiterOptions = Omit<HonoConfigProps, 'keyGenerator'> & {\n\t/**\n\t * Either a standard `redis://`/`rediss://` connection string (backed by\n\t * `ioredis`, for a self-hosted Redis) or an Upstash REST URL\n\t * (`https://...`, backed by `@upstash/redis`, requires `redisToken`).\n\t */\n\tredisUrl?: string;\n\t/** Upstash REST token — only used when `redisUrl` is an Upstash URL. */\n\tredisToken?: string;\n\t/**\n\t * Redis key prefix for this limiter's counters. `RedisStore` defaults to\n\t * a fixed `\"hrl:\"` prefix, so two `rateLimiter()` instances pointed at\n\t * the same Redis (e.g. a global limiter and a stricter per-route one)\n\t * silently share counters unless given distinct prefixes here.\n\t */\n\tprefix?: string;\n\tkeyGenerator?: HonoConfigProps['keyGenerator'];\n};\n\n/**\n * Adapts a standard `ioredis` client to the bespoke `RedisClient` shape\n * `hono-rate-limiter`'s `RedisStore` expects (mirroring what `@upstash/redis`\n * natively implements), so a plain self-hosted Redis (`redis://...`) can back\n * the same `RedisStore` used for Upstash.\n */\nfunction ioredisAdapter(client: IORedis): RedisClient {\n\treturn {\n\t\tscriptLoad: (script) => client.script('LOAD', script) as Promise<string>,\n\t\tevalsha: (sha1, keys, args) =>\n\t\t\tclient.evalsha(sha1, keys.length, ...keys, ...(args as string[])) as any,\n\t\tdecr: (key) => client.decr(key),\n\t\tdel: (key) => client.del(key),\n\t};\n}\n\nfunction redisClientFor(redisUrl: string, redisToken?: string): RedisClient {\n\tif (redisUrl.startsWith('redis://') || redisUrl.startsWith('rediss://')) {\n\t\tconst client = new IORedis(redisUrl);\n\t\tclient.on('error', (err) => {\n\t\t\tlogger.error(`[rate-limiter] Redis connection error: ${err.message}`);\n\t\t});\n\t\treturn ioredisAdapter(client);\n\t}\n\treturn new UpstashRedis({\n\t\turl: redisUrl,\n\t\ttoken: redisToken,\n\t}) as unknown as RedisClient;\n}\n\nexport function rateLimiter(options?: RateLimiterOptions) {\n\treturn honoRateLimiter({\n\t\t...options,\n\t\twindowMs: options?.windowMs ?? 1 * 60 * 1000,\n\t\tlimit: options?.limit ?? 10,\n\t\tkeyGenerator:\n\t\t\toptions?.keyGenerator ?? ((c) => c.req.header('x-forwarded-for') ?? ''),\n\t\tstore: options?.redisUrl\n\t\t\t? new RedisStore({\n\t\t\t\t\tclient: redisClientFor(options.redisUrl, options.redisToken),\n\t\t\t\t\tprefix: options.prefix,\n\t\t\t\t})\n\t\t\t: undefined,\n\t});\n}\n",
|
|
13
|
+
"import { isScopeAuthority } from '@nxgt/shared/helpers';\nimport { CustomException } from '@nxgt/shared-exceptions';\nimport { getLogger } from '@nxgt/shared-logging';\nimport { createMiddleware } from 'hono/factory';\nimport type { MiddlewareHandler } from 'hono/types';\n\n/**\n * Route guard middleware following Apollo Federation requireScopes semantics.\n *\n * `authorities` is an **array of groups** (array of arrays):\n * - Outer array = AND — every group must be satisfied.\n * - Inner array = OR — at least one authority in the group must match.\n *\n * Examples:\n * secured() — authentication check only\n * secured([['ADMIN', 'users:read']]) — ADMIN or users:read\n * secured([['ADMIN'], ['users:read']]) — ADMIN and users:read\n *\n * For confidential-client principals (clientId present, no username) only\n * SCOPE_* authorities are considered — role/permission entries are ignored.\n */\nexport function secured(authorities: string[][] = []): MiddlewareHandler {\n\treturn createMiddleware(async (ctx, next) => {\n\t\tconst logger = ctx.get('logger') || getLogger();\n\n\t\tlogger.info('Secured middleware: checking authorities');\n\t\tconst user = ctx.get('principal');\n\n\t\tif (!user) {\n\t\t\tlogger.error('Unauthenticated access attempt');\n\t\t\tthrow CustomException.unauthorized({\n\t\t\t\tmessage: 'errors.unauthenticated',\n\t\t\t});\n\t\t}\n\n\t\tif (user.roles?.includes('ADMIN')) {\n\t\t\treturn next();\n\t\t}\n\n\t\tlogger.info(\n\t\t\t`User authenticated: ${user.name ?? user.username ?? user.clientId}`,\n\t\t);\n\n\t\tif (!authorities.length) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// For confidential clients, only SCOPE_* authorities are considered —\n\t\t// role/permission entries are ignored. If the client lacks the required\n\t\t// scope authority it is denied.\n\t\tconst isClient = !!user.clientId && !user.username;\n\t\tconst userAuthorities = user.authorities ?? [];\n\t\tconst effectiveUserAuthorities = isClient\n\t\t\t? userAuthorities.filter(isScopeAuthority)\n\t\t\t: userAuthorities;\n\n\t\tconst granted = authorities.every((group) => {\n\t\t\tif (group.length === 0) return true;\n\t\t\treturn group.some((authority) =>\n\t\t\t\teffectiveUserAuthorities.includes(authority),\n\t\t\t);\n\t\t});\n\n\t\tif (granted) {\n\t\t\tawait next();\n\t\t\treturn;\n\t\t}\n\n\t\tthrow CustomException.forbidden({\n\t\t\tmessage: 'errors.forbidden',\n\t\t});\n\t});\n}\n"
|
|
14
|
+
],
|
|
15
|
+
"mappings": "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",
|
|
16
|
+
"debugId": "EC87DAEAD6B394CA64756E2164756E21",
|
|
17
|
+
"names": []
|
|
18
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type McpServer, type WebStandardStreamableHTTPServerTransportOptions } from '@modelcontextprotocol/server';
|
|
2
|
+
export declare function createMcpServerApp(server: McpServer, options?: WebStandardStreamableHTTPServerTransportOptions): import("hono").Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
3
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/mcp/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,SAAS,EAEd,KAAK,+CAA+C,EACpD,MAAM,8BAA8B,CAAC;AActC,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,SAAS,EACjB,OAAO,CAAC,EAAE,+CAA+C,6FAqCzD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/mcp/index.ts
|
|
2
|
+
export * from "@modelcontextprotocol/hono";
|
|
3
|
+
export * from "@modelcontextprotocol/server";
|
|
4
|
+
|
|
5
|
+
// src/mcp/helpers.ts
|
|
6
|
+
import { createMcpHonoApp } from "@modelcontextprotocol/hono";
|
|
7
|
+
import {
|
|
8
|
+
WebStandardStreamableHTTPServerTransport
|
|
9
|
+
} from "@modelcontextprotocol/server";
|
|
10
|
+
import { createAuthClient } from "stx-sdk/auth";
|
|
11
|
+
var auth = createAuthClient("http://localhost:8080/api");
|
|
12
|
+
function createMcpServerApp(server, options) {
|
|
13
|
+
const app = createMcpHonoApp();
|
|
14
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
15
|
+
...options,
|
|
16
|
+
sessionIdGenerator: options?.sessionIdGenerator || (() => Bun.randomUUIDv7())
|
|
17
|
+
});
|
|
18
|
+
app.all("/mcp", async (c) => {
|
|
19
|
+
if (!server.isConnected()) {
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
}
|
|
22
|
+
const authHeader = c.req.header("Authorization");
|
|
23
|
+
const result = await auth.POST("/oauth/introspect", {
|
|
24
|
+
body: { token: authHeader?.split(" ")[1] ?? "" }
|
|
25
|
+
});
|
|
26
|
+
if (!result.data?.active) {
|
|
27
|
+
c.status(401);
|
|
28
|
+
return c.json({ error: "MCP Unauthorized" });
|
|
29
|
+
}
|
|
30
|
+
return transport.handleRequest(c.req.raw, {
|
|
31
|
+
authInfo: {
|
|
32
|
+
scopes: result.data.scope?.split(" ") ?? [],
|
|
33
|
+
clientId: result.data.clientId ?? "",
|
|
34
|
+
token: authHeader?.split(" ")[1] ?? "",
|
|
35
|
+
extra: result.data
|
|
36
|
+
},
|
|
37
|
+
parsedBody: c.get("parsedBody")
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return app;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createMcpServerApp
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//# debugId=4B556E93F95CB6CA64756E2164756E21
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/mcp/index.ts", "../src/mcp/helpers.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// The two star re-exports live here, in the entry point, and not in\n// `helpers.ts`: Bun emits `__reExport(ns, hono)` with `hono` undeclared when a\n// star re-export of an external package sits below the entry. See AGENTS.md.\nexport * from '@modelcontextprotocol/hono';\nexport * from '@modelcontextprotocol/server';\nexport * from './helpers';\n",
|
|
6
|
+
"import { createMcpHonoApp } from '@modelcontextprotocol/hono';\nimport {\n\ttype McpServer,\n\tWebStandardStreamableHTTPServerTransport,\n\ttype WebStandardStreamableHTTPServerTransportOptions,\n} from '@modelcontextprotocol/server';\nimport type { Context } from 'hono';\n\nimport { createAuthClient } from 'stx-sdk/auth';\n\n// Not exported: nothing outside this module used it, and its inferred type\n// reaches openapi-fetch through stx-sdk's own node_modules — a path that does\n// not exist for anyone installing this package, which `tsc` refuses to write\n// into a declaration (TS2883).\n//\n// The base URL is hardcoded, which is wrong for a shared package; left as it\n// was rather than changed silently during the merge.\nconst auth = createAuthClient('http://localhost:8080/api');\n\nexport function createMcpServerApp(\n\tserver: McpServer,\n\toptions?: WebStandardStreamableHTTPServerTransportOptions,\n) {\n\tconst app = createMcpHonoApp();\n\n\tconst transport = new WebStandardStreamableHTTPServerTransport({\n\t\t...options,\n\t\tsessionIdGenerator:\n\t\t\toptions?.sessionIdGenerator || (() => Bun.randomUUIDv7()),\n\t});\n\n\tapp.all('/mcp', async (c) => {\n\t\tif (!server.isConnected()) {\n\t\t\tawait server.connect(transport);\n\t\t}\n\n\t\tconst authHeader = c.req.header('Authorization');\n\n\t\tconst result = await auth.POST('/oauth/introspect', {\n\t\t\tbody: { token: authHeader?.split(' ')[1] ?? '' },\n\t\t});\n\n\t\tif (!result.data?.active) {\n\t\t\tc.status(401);\n\t\t\treturn c.json({ error: 'MCP Unauthorized' });\n\t\t}\n\n\t\treturn transport.handleRequest(c.req.raw, {\n\t\t\tauthInfo: {\n\t\t\t\tscopes: result.data.scope?.split(' ') ?? [],\n\t\t\t\tclientId: result.data.clientId ?? '',\n\t\t\t\ttoken: authHeader?.split(' ')[1] ?? '',\n\t\t\t\textra: result.data,\n\t\t\t},\n\t\t\tparsedBody: (c as Context).get('parsedBody'),\n\t\t});\n\t});\n\treturn app;\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": ";AAGA;AACA;;;ACJA;AACA;AAAA;AAAA;AAOA;AASA,IAAM,OAAO,iBAAiB,2BAA2B;AAElD,SAAS,kBAAkB,CACjC,QACA,SACC;AAAA,EACD,MAAM,MAAM,iBAAiB;AAAA,EAE7B,MAAM,YAAY,IAAI,yCAAyC;AAAA,OAC3D;AAAA,IACH,oBACC,SAAS,uBAAuB,MAAM,IAAI,aAAa;AAAA,EACzD,CAAC;AAAA,EAED,IAAI,IAAI,QAAQ,OAAO,MAAM;AAAA,IAC5B,IAAI,CAAC,OAAO,YAAY,GAAG;AAAA,MAC1B,MAAM,OAAO,QAAQ,SAAS;AAAA,IAC/B;AAAA,IAEA,MAAM,aAAa,EAAE,IAAI,OAAO,eAAe;AAAA,IAE/C,MAAM,SAAS,MAAM,KAAK,KAAK,qBAAqB;AAAA,MACnD,MAAM,EAAE,OAAO,YAAY,MAAM,GAAG,EAAE,MAAM,GAAG;AAAA,IAChD,CAAC;AAAA,IAED,IAAI,CAAC,OAAO,MAAM,QAAQ;AAAA,MACzB,EAAE,OAAO,GAAG;AAAA,MACZ,OAAO,EAAE,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAAA,IAC5C;AAAA,IAEA,OAAO,UAAU,cAAc,EAAE,IAAI,KAAK;AAAA,MACzC,UAAU;AAAA,QACT,QAAQ,OAAO,KAAK,OAAO,MAAM,GAAG,KAAK,CAAC;AAAA,QAC1C,UAAU,OAAO,KAAK,YAAY;AAAA,QAClC,OAAO,YAAY,MAAM,GAAG,EAAE,MAAM;AAAA,QACpC,OAAO,OAAO;AAAA,MACf;AAAA,MACA,YAAa,EAAc,IAAI,YAAY;AAAA,IAC5C,CAAC;AAAA,GACD;AAAA,EACD,OAAO;AAAA;",
|
|
9
|
+
"debugId": "4B556E93F95CB6CA64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Media types accepted in a QUERY request body, advertised by `acceptQuery()`. */
|
|
2
|
+
export declare const ACCEPT_QUERY_MEDIA_TYPE = "application/json";
|
|
3
|
+
/**
|
|
4
|
+
* Advertises QUERY support on a resource.
|
|
5
|
+
*
|
|
6
|
+
* `Accept-Query` is how the safe-method-with-body draft says a resource
|
|
7
|
+
* announces both that it answers QUERY *and* which media types it will accept
|
|
8
|
+
* in the request content. It rides on the QUERY registration only: the
|
|
9
|
+
* `POST …/search` route it shares its handlers with is left exactly as it was,
|
|
10
|
+
* headers included, so that every existing caller sees no change at all.
|
|
11
|
+
*
|
|
12
|
+
* Set after `next()` so it lands on whatever the handler produced, errors
|
|
13
|
+
* included.
|
|
14
|
+
*/
|
|
15
|
+
export declare function acceptQuery(mediaTypes?: string): import("hono").MiddlewareHandler<any, string, {}, Response>;
|
|
16
|
+
//# sourceMappingURL=accept-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accept-query.d.ts","sourceRoot":"","sources":["../../src/middlewares/accept-query.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,UAAU,GAAE,MAAgC,+DAKvE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-user.d.ts","sourceRoot":"","sources":["../../src/middlewares/current-user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,WAAW,QAAO,iBAuD5B,CAAC;AAEJ,OAAO,EAAE,KAAK,SAAS,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LocaleKey } from '@nxgt/i18n';
|
|
2
|
+
import type { ErrorHandler } from 'hono';
|
|
3
|
+
export type ErrorHandlerOptions = {
|
|
4
|
+
showStackInDev?: boolean;
|
|
5
|
+
showStackInTest?: boolean;
|
|
6
|
+
logToConsole?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const createErrorHandler: <K extends LocaleKey = LocaleKey>(translate: (key: K, context?: Record<string, any>) => string, { showStackInDev, showStackInTest, logToConsole, }?: ErrorHandlerOptions) => ErrorHandler;
|
|
9
|
+
//# sourceMappingURL=error-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/middlewares/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIzC,MAAM,MAAM,mBAAmB,GAAG;IACjC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,SAAS,GAAG,SAAS,EACjE,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,MAAM,EAC5D,qDAIG,mBAAwB,KACzB,YAwCF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './accept-query';
|
|
2
|
+
export * from './current-user';
|
|
3
|
+
export * from './error-handler';
|
|
4
|
+
export * from './openfetch-service-user';
|
|
5
|
+
export * from './ory-auth';
|
|
6
|
+
export * from './rate-limiter';
|
|
7
|
+
export * from './secured';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middlewares/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openfetch-service-user.d.ts","sourceRoot":"","sources":["../../src/middlewares/openfetch-service-user.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,wBAAgB,oBAAoB,IAAI,UAAU,CA+CjD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Principal } from '@nxgt/shared/models';
|
|
2
|
+
import { CustomException } from '@nxgt/shared-exceptions';
|
|
3
|
+
import type { ErrorHandler } from 'hono';
|
|
4
|
+
import { type Ory, type OryPrincipal, OryUnavailable } from 'stx-sdk/ory';
|
|
5
|
+
/**
|
|
6
|
+
* Authentication for an Ory-native API — the twin of storex-api's
|
|
7
|
+
* `remoteAuth()`, with the Ory stack instead of oauth-api as the authority.
|
|
8
|
+
* Same contract, so `policyGuard` and a `rules.yaml` keep answering 401 for
|
|
9
|
+
* `authenticated: true` without knowing which authority signed the caller in:
|
|
10
|
+
*
|
|
11
|
+
* - `NODE_ENV=test` and `X-User-*` headers present ⇒ the mock principal, as
|
|
12
|
+
* every route spec in this repo expects. The Ory principal is synthesised
|
|
13
|
+
* from it so `<module>.access.ts` sees a `subject` either way.
|
|
14
|
+
* - no credential, or one Kratos / Hydra does not honour ⇒ `next()` as an
|
|
15
|
+
* anonymous caller. The rules file decides whether that is a 401.
|
|
16
|
+
* - Kratos, Hydra or Keto unreachable ⇒ **503**, fail closed. Never an
|
|
17
|
+
* anonymous `next()`: that turns an outage into a lockout with no error.
|
|
18
|
+
* - otherwise `principal` (the repo-wide shape), `ory` (the Ory one —
|
|
19
|
+
* `subject` is what Keto receives), `accessToken` (the Bearer, if that is
|
|
20
|
+
* what came in) and `X-Claims`.
|
|
21
|
+
*
|
|
22
|
+
* Takes an `Ory` rather than URLs so the app builds one `createOry()` from
|
|
23
|
+
* its own zod-validated env and shares it with its access layer.
|
|
24
|
+
*/
|
|
25
|
+
export declare function oryAuth(ory: Ory): import("hono").MiddlewareHandler<any, string, {}, Response>;
|
|
26
|
+
/**
|
|
27
|
+
* The 503 an `OryUnavailable` becomes — for the middleware above and for
|
|
28
|
+
* `withOryUnavailable` below, so a Keto outage in a service's `isAllowed`
|
|
29
|
+
* answers the same thing as a Kratos outage in `resolve`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function serviceUnavailable(error: OryUnavailable): CustomException;
|
|
32
|
+
/**
|
|
33
|
+
* Wraps the app's error handler so an `OryUnavailable` thrown anywhere
|
|
34
|
+
* below the middleware — an access layer asking Keto, a service listing
|
|
35
|
+
* tuples — is a 503 and not the generic 500. Without it the middleware's
|
|
36
|
+
* own mapping only covers `resolve`, and a Keto restart would surface as
|
|
37
|
+
* "Internal server error" from every guarded route:
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* app.onError(withOryUnavailable(createErrorHandler(translate)));
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function withOryUnavailable(handler: ErrorHandler): ErrorHandler;
|
|
44
|
+
/**
|
|
45
|
+
* The repo-wide `Principal` from an Ory one. `id` is the subject — the
|
|
46
|
+
* identity id or the client id — because `id` is what every existing
|
|
47
|
+
* `ownerId` comparison reads, and a tuple written for `subject` must match
|
|
48
|
+
* an ownership check written for `id`. No authorities and no roles: Keto
|
|
49
|
+
* answers those questions per object, and an empty list is what stops a
|
|
50
|
+
* `@policy`-style check from granting anything by accident.
|
|
51
|
+
*/
|
|
52
|
+
export declare function toPrincipal(ory: OryPrincipal): Principal;
|
|
53
|
+
//# sourceMappingURL=ory-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ory-auth.d.ts","sourceRoot":"","sources":["../../src/middlewares/ory-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,EAEN,KAAK,GAAG,EACR,KAAK,YAAY,EACjB,cAAc,EACd,MAAM,aAAa,CAAC;AAIrB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,+DAsD/B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CASzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,CAexD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type HonoConfigProps } from 'hono-rate-limiter';
|
|
2
|
+
export type RateLimiterOptions = Omit<HonoConfigProps, 'keyGenerator'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Either a standard `redis://`/`rediss://` connection string (backed by
|
|
5
|
+
* `ioredis`, for a self-hosted Redis) or an Upstash REST URL
|
|
6
|
+
* (`https://...`, backed by `@upstash/redis`, requires `redisToken`).
|
|
7
|
+
*/
|
|
8
|
+
redisUrl?: string;
|
|
9
|
+
/** Upstash REST token — only used when `redisUrl` is an Upstash URL. */
|
|
10
|
+
redisToken?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Redis key prefix for this limiter's counters. `RedisStore` defaults to
|
|
13
|
+
* a fixed `"hrl:"` prefix, so two `rateLimiter()` instances pointed at
|
|
14
|
+
* the same Redis (e.g. a global limiter and a stricter per-route one)
|
|
15
|
+
* silently share counters unless given distinct prefixes here.
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
keyGenerator?: HonoConfigProps['keyGenerator'];
|
|
19
|
+
};
|
|
20
|
+
export declare function rateLimiter(options?: RateLimiterOptions): import("hono").MiddlewareHandler<import("hono").Env, string, import("hono").Input, Response>;
|
|
21
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/middlewares/rate-limiter.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,eAAe,EAIpB,MAAM,mBAAmB,CAAC;AAG3B,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG;IACxE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;CAC/C,CAAC;AAgCF,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,gGAcvD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono/types';
|
|
2
|
+
/**
|
|
3
|
+
* Route guard middleware following Apollo Federation requireScopes semantics.
|
|
4
|
+
*
|
|
5
|
+
* `authorities` is an **array of groups** (array of arrays):
|
|
6
|
+
* - Outer array = AND — every group must be satisfied.
|
|
7
|
+
* - Inner array = OR — at least one authority in the group must match.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* secured() — authentication check only
|
|
11
|
+
* secured([['ADMIN', 'users:read']]) — ADMIN or users:read
|
|
12
|
+
* secured([['ADMIN'], ['users:read']]) — ADMIN and users:read
|
|
13
|
+
*
|
|
14
|
+
* For confidential-client principals (clientId present, no username) only
|
|
15
|
+
* SCOPE_* authorities are considered — role/permission entries are ignored.
|
|
16
|
+
*/
|
|
17
|
+
export declare function secured(authorities?: string[][]): MiddlewareHandler;
|
|
18
|
+
//# sourceMappingURL=secured.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secured.d.ts","sourceRoot":"","sources":["../../src/middlewares/secured.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CAAC,WAAW,GAAE,MAAM,EAAE,EAAO,GAAG,iBAAiB,CAmDvE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-fetch.d.ts","sourceRoot":"","sources":["../src/openapi-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAC;AAEzC,cAAc,eAAe,CAAC;AAE9B,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/openapi-fetch.ts
|
|
2
|
+
import createClient from "openapi-fetch";
|
|
3
|
+
|
|
4
|
+
export * from "openapi-fetch";
|
|
5
|
+
var openapi_fetch_default = createClient;
|
|
6
|
+
export {
|
|
7
|
+
openapi_fetch_default as default
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
//# debugId=925B8E246A2C7E0D64756E2164756E21
|
|
11
|
+
//# sourceMappingURL=openapi-fetch.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/openapi-fetch.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import createClient from 'openapi-fetch';\n\nexport * from 'openapi-fetch';\n\nexport default createClient;\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAAA;AAAA;AAEA;AAEA,IAAe;",
|
|
8
|
+
"debugId": "925B8E246A2C7E0D64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Principal } from '@nxgt/shared/models';
|
|
2
|
+
import type { OryPrincipal } from 'stx-sdk/ory';
|
|
3
|
+
|
|
4
|
+
declare module 'hono' {
|
|
5
|
+
interface ContextVariableMap {
|
|
6
|
+
principal?: Principal | null;
|
|
7
|
+
/** The Bearer token the caller sent, when that is how they signed in. */
|
|
8
|
+
accessToken?: string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Set by `oryAuth()`: the Ory principal, `null` for an anonymous
|
|
11
|
+
* caller. `ory.subject` is the string Keto receives.
|
|
12
|
+
*/
|
|
13
|
+
ory?: OryPrincipal | null;
|
|
14
|
+
'X-User-Id'?: string | null;
|
|
15
|
+
'X-User-Name'?: string | null;
|
|
16
|
+
'X-User-Email'?: string | null;
|
|
17
|
+
'X-User-Firstname'?: string | null;
|
|
18
|
+
'X-User-Lastname'?: string | null;
|
|
19
|
+
'X-User-Birthdate'?: string | null;
|
|
20
|
+
'X-User-Authorities'?: string[] | null;
|
|
21
|
+
'X-Roles'?: string[] | null;
|
|
22
|
+
'X-Realm'?: string | null;
|
|
23
|
+
'X-Scopes'?: string[] | null;
|
|
24
|
+
'X-Client-Id'?: string | null;
|
|
25
|
+
'X-Claims'?: Record<string, any> | null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Principal } from '@nxgt/shared/models';
|
|
2
|
+
import type { Context } from 'hono';
|
|
3
|
+
import type { Middleware } from 'openapi-fetch';
|
|
4
|
+
/**
|
|
5
|
+
* Inverse of `mockAuthMiddleware`: reads the `X-User-*` headers it injects
|
|
6
|
+
* back into a `Principal`. For services that verify tokens themselves
|
|
7
|
+
* (zero-trust, no gateway in front) but still want their existing route
|
|
8
|
+
* specs — written against `mockAuthMiddleware` — to work unchanged in
|
|
9
|
+
* NODE_ENV=test. Returns `undefined` when no mock headers are present, so
|
|
10
|
+
* callers can fall back to real token verification.
|
|
11
|
+
*/
|
|
12
|
+
export declare function principalFromMockHeaders(ctx: Context): Principal | undefined;
|
|
13
|
+
export declare function mockUser(values: Omit<Principal, 'authorities' | 'id'> & {
|
|
14
|
+
permissions?: string[];
|
|
15
|
+
roles?: string[];
|
|
16
|
+
}): Principal;
|
|
17
|
+
export declare function mockAuthMiddleware(user: Principal): Middleware;
|
|
18
|
+
/**
|
|
19
|
+
* openapi-fetch middleware that turns a `POST <resource>/search` call into the
|
|
20
|
+
* `QUERY <resource>` it mirrors — same body, same response, safe method.
|
|
21
|
+
*
|
|
22
|
+
* The generated client cannot express QUERY: `openapi-typescript` has no
|
|
23
|
+
* `query` path-item key (its method list is the eight classic verbs), so the
|
|
24
|
+
* operation is invisible to the `paths` type, and `openapi-fetch`'s own
|
|
25
|
+
* `request()` is constrained to `HttpMethod`. Call the POST search operation
|
|
26
|
+
* for the types — request and response are identical either way — and let this
|
|
27
|
+
* rewrite the verb *and* drop the `/search` suffix on the wire:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* client.use(mockAuthMiddleware(principal));
|
|
31
|
+
* const { data, error } = await client.POST('/tags/search', {
|
|
32
|
+
* body: {},
|
|
33
|
+
* middleware: [asQueryMethod],
|
|
34
|
+
* }); // actually sends: QUERY /tags
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Per-request middleware runs after the ones registered with `client.use()`,
|
|
38
|
+
* so headers set by `mockAuthMiddleware` are already on the request and are
|
|
39
|
+
* carried over. The body is read to a string rather than passed as a stream:
|
|
40
|
+
* a streaming body would need `duplex: 'half'`, and these are small JSON
|
|
41
|
+
* payloads.
|
|
42
|
+
*/
|
|
43
|
+
export declare const asQueryMethod: Middleware;
|
|
44
|
+
//# sourceMappingURL=test.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.utils.d.ts","sourceRoot":"","sources":["../../src/utils/test.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,qBAAqB,CAAC;AAEnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAwB5E;AAED,wBAAgB,QAAQ,CACvB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC,GAAG;IAC/C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GACC,SAAS,CAOX;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAmB9D;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,aAAa,EAAE,UAW3B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nxgt/shared-hono",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"package.json"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./openapi-fetch": {
|
|
20
|
+
"types": "./dist/openapi-fetch.d.ts",
|
|
21
|
+
"import": "./dist/openapi-fetch.js",
|
|
22
|
+
"default": "./dist/openapi-fetch.js"
|
|
23
|
+
},
|
|
24
|
+
"./mcp": {
|
|
25
|
+
"types": "./dist/mcp/index.d.ts",
|
|
26
|
+
"import": "./dist/mcp/index.js",
|
|
27
|
+
"default": "./dist/mcp/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/softistx/nxgt-core.git",
|
|
34
|
+
"directory": "packages/shared-hono"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org",
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "bun run ../../build.ts",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"nxgt": {
|
|
45
|
+
"entrypoints": [
|
|
46
|
+
"src/index.ts",
|
|
47
|
+
"src/openapi-fetch.ts",
|
|
48
|
+
"src/mcp/index.ts"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@hono/zod-validator": "^0.9.0",
|
|
53
|
+
"@modelcontextprotocol/hono": "^2.0.0",
|
|
54
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
55
|
+
"@nxgt/i18n": "1.0.0",
|
|
56
|
+
"@nxgt/shared": "1.0.0",
|
|
57
|
+
"@nxgt/shared-exceptions": "1.0.0",
|
|
58
|
+
"@nxgt/shared-logging": "1.0.0",
|
|
59
|
+
"@nxgt/shared-mongo": "1.0.0",
|
|
60
|
+
"@upstash/redis": "^1.38.2",
|
|
61
|
+
"hono": "^4.13.4",
|
|
62
|
+
"hono-rate-limiter": "^0.5.3",
|
|
63
|
+
"ioredis": "^6.0.0",
|
|
64
|
+
"lodash": "^4.18.1",
|
|
65
|
+
"openapi-fetch": "^0.17.0",
|
|
66
|
+
"zod": "^4.4.3"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/bun": "^1.4.0",
|
|
70
|
+
"@types/lodash": "^4.17.25",
|
|
71
|
+
"jose": "^6.2.9"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"stx-sdk": ">=1.0.0",
|
|
75
|
+
"typescript": "^6.0.3"
|
|
76
|
+
}
|
|
77
|
+
}
|