@pands/gateway 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @pands/gateway
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Reverse proxy gateway, OpenAPI specification merger, rate limiting, and internal JWT signer for the **Pand Application Platform**.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@pands/gateway.svg)](https://www.npmjs.com/package/@pands/gateway)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ `@pands/gateway` sits in front of all microservices, routing incoming requests, managing public-to-private API namespaces, enforcing rate limits, validating session authentication, and injecting verified internal identity JWTs.
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @pands/gateway
16
+ # or with npm:
17
+ npm install @pands/gateway
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Usage
23
+
24
+ ### Create Gateway App
25
+
26
+ ```typescript
27
+ import { createGatewayApp } from "@pands/gateway";
28
+
29
+ const app = createGatewayApp({
30
+ title: "Acme API Gateway",
31
+ version: "0.1.0",
32
+ services: {
33
+ core: { target: "http://localhost:3010", prefix: "/api/v1/core" },
34
+ billing: { target: "http://localhost:3011", prefix: "/api/v1/billing" },
35
+ },
36
+ authServiceTarget: "http://localhost:3010",
37
+ internalSecret: process.env.PAND_INTERNAL_SECRET || "dev-internal-secret",
38
+ });
39
+
40
+ export default app;
41
+ ```
42
+
43
+ ---
44
+
45
+ ## License
46
+
47
+ MIT © [Mehdi Nosrati](https://github.com/MehdiNosrati)
@@ -0,0 +1,65 @@
1
+ import * as _pands_logger from '@pands/logger';
2
+ import * as hono_types from 'hono/types';
3
+ import { PandAuthInstance } from '@pands/auth';
4
+ import { PandConfig } from '@pands/config';
5
+ export { serveService } from '@pands/service';
6
+ import { Hono } from 'hono';
7
+
8
+ interface ServiceOpenApiInput {
9
+ service: string;
10
+ spec: Record<string, any>;
11
+ }
12
+ declare function mergeServiceOpenApiSpecs(serviceSpecs: ServiceOpenApiInput[], info?: {
13
+ title?: string;
14
+ version?: string;
15
+ }): Record<string, any>;
16
+
17
+ interface PublicClientConfig {
18
+ project: string;
19
+ displayName: string;
20
+ env: string;
21
+ apiBaseUrl: string;
22
+ authBaseUrl: string;
23
+ posthog?: {
24
+ publicKey: string;
25
+ host: string;
26
+ };
27
+ links?: {
28
+ grafanaUrl?: string;
29
+ posthogUrl?: string;
30
+ apiDocsUrl: string;
31
+ };
32
+ }
33
+ interface ResolvedActor {
34
+ userId: string;
35
+ email: string;
36
+ role: "user" | "admin";
37
+ }
38
+ interface CreateGatewayAppOptions {
39
+ config: PandConfig;
40
+ serviceUrls?: Record<string, string>;
41
+ serviceApps?: Record<string, {
42
+ fetch: (req: Request) => Response | Promise<Response>;
43
+ }>;
44
+ auth?: PandAuthInstance;
45
+ databaseUrl?: string;
46
+ resolveSession?: (req: Request) => Promise<ResolvedActor | null>;
47
+ authHandler?: (req: Request) => Response | Promise<Response>;
48
+ dbHealth?: () => Promise<string>;
49
+ internalSecret?: string;
50
+ posthogFetch?: typeof fetch;
51
+ staticRootDir?: string;
52
+ }
53
+ declare function resolveGatewayServiceUrls(config: PandConfig, explicitUrls?: Record<string, string>): Record<string, string>;
54
+ declare function createGatewayApp(options: CreateGatewayAppOptions): Hono<{
55
+ Variables: {
56
+ requestId: string;
57
+ };
58
+ }, hono_types.BlankSchema, "/"> & {
59
+ serviceName: string;
60
+ logger: _pands_logger.Logger;
61
+ registerAdminEntity: () => void;
62
+ getAdminEntities: () => never[];
63
+ };
64
+
65
+ export { type CreateGatewayAppOptions, type PublicClientConfig, type ResolvedActor, type ServiceOpenApiInput, createGatewayApp, mergeServiceOpenApiSpecs, resolveGatewayServiceUrls };
package/dist/index.js ADDED
@@ -0,0 +1,469 @@
1
+ // src/index.ts
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import {
6
+ authTables,
7
+ createPandAuth,
8
+ resolveRequestActor
9
+ } from "@pands/auth";
10
+ import { resolveProjectPorts } from "@pands/config";
11
+ import { createDb } from "@pands/db";
12
+ import { createLogger } from "@pands/logger";
13
+ import { PandError, serveService, signInternalToken } from "@pands/service";
14
+ import { apiReference } from "@scalar/hono-api-reference";
15
+ import { Hono } from "hono";
16
+
17
+ // src/openapi-merge.ts
18
+ function toPascalCase(input) {
19
+ return input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
20
+ }
21
+ function rewriteSchemaRefs(obj, renameMap) {
22
+ if (Array.isArray(obj)) {
23
+ return obj.map((item) => rewriteSchemaRefs(item, renameMap));
24
+ }
25
+ if (obj && typeof obj === "object") {
26
+ const out = {};
27
+ for (const [k, v] of Object.entries(obj)) {
28
+ if (k === "$ref" && typeof v === "string" && v.startsWith("#/components/schemas/")) {
29
+ const schemaName = v.slice("#/components/schemas/".length);
30
+ const mapped = renameMap[schemaName] ?? schemaName;
31
+ out[k] = `#/components/schemas/${mapped}`;
32
+ } else {
33
+ out[k] = rewriteSchemaRefs(v, renameMap);
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ return obj;
39
+ }
40
+ function mergeServiceOpenApiSpecs(serviceSpecs, info = {}) {
41
+ const mergedPaths = {};
42
+ const mergedSchemas = {};
43
+ const internalPaths = /* @__PURE__ */ new Set(["/healthz", "/readyz", "/openapi.json", "/_pand/meta"]);
44
+ for (const { service, spec } of serviceSpecs) {
45
+ const pascalService = toPascalCase(service);
46
+ const renameMap = {};
47
+ const schemas = spec?.components?.schemas ?? {};
48
+ for (const [schemaName, schemaDef] of Object.entries(schemas)) {
49
+ if (mergedSchemas[schemaName]) {
50
+ const prefixed = `${pascalService}_${schemaName}`;
51
+ renameMap[schemaName] = prefixed;
52
+ mergedSchemas[prefixed] = schemaDef;
53
+ } else {
54
+ mergedSchemas[schemaName] = schemaDef;
55
+ }
56
+ }
57
+ const rewrittenSpec = Object.keys(renameMap).length > 0 ? rewriteSchemaRefs(spec, renameMap) : spec;
58
+ const rewrittenSchemas = rewrittenSpec?.components?.schemas ?? {};
59
+ for (const [schemaName, schemaDef] of Object.entries(rewrittenSchemas)) {
60
+ const targetName = renameMap[schemaName] ?? schemaName;
61
+ mergedSchemas[targetName] = schemaDef;
62
+ }
63
+ const paths = rewrittenSpec?.paths ?? {};
64
+ for (const [rawPath, pathItem] of Object.entries(paths)) {
65
+ if (internalPaths.has(rawPath)) {
66
+ continue;
67
+ }
68
+ const normalized = rawPath === "/" ? "" : rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
69
+ const prefixedPath = `/api/v1/${service}${normalized}`;
70
+ mergedPaths[prefixedPath] = pathItem;
71
+ }
72
+ }
73
+ return {
74
+ openapi: "3.1.0",
75
+ info: {
76
+ title: info.title ?? "Pand API",
77
+ version: info.version ?? "0.1.0"
78
+ },
79
+ paths: mergedPaths,
80
+ components: {
81
+ schemas: mergedSchemas
82
+ }
83
+ };
84
+ }
85
+
86
+ // src/index.ts
87
+ function defaultResolveSession(req) {
88
+ const testUserId = req.headers.get("x-test-user-id");
89
+ if (testUserId) {
90
+ const roleHeader = req.headers.get("x-test-user-role");
91
+ return Promise.resolve({
92
+ userId: testUserId,
93
+ email: req.headers.get("x-test-user-email") ?? `${testUserId}@mahdins.dev`,
94
+ role: roleHeader === "admin" ? "admin" : "user"
95
+ });
96
+ }
97
+ const authHeader = req.headers.get("authorization");
98
+ if (authHeader?.startsWith("Bearer test-user:")) {
99
+ const parts = authHeader.slice("Bearer test-user:".length).split(":");
100
+ const userId = parts[0] || "user-1";
101
+ const role = parts[1] === "admin" ? "admin" : "user";
102
+ return Promise.resolve({
103
+ userId,
104
+ email: `${userId}@mahdins.dev`,
105
+ role
106
+ });
107
+ }
108
+ return Promise.resolve(null);
109
+ }
110
+ function resolveGatewayServiceUrls(config, explicitUrls) {
111
+ if (explicitUrls) {
112
+ return explicitUrls;
113
+ }
114
+ if (process.env.PAND_SERVICE_URLS) {
115
+ return JSON.parse(process.env.PAND_SERVICE_URLS);
116
+ }
117
+ const ports = resolveProjectPorts(config);
118
+ const urls = {};
119
+ for (const [svc, port] of Object.entries(ports.services)) {
120
+ urls[svc] = `http://localhost:${port}`;
121
+ }
122
+ return urls;
123
+ }
124
+ function createGatewayApp(options) {
125
+ const { config } = options;
126
+ const serviceUrls = resolveGatewayServiceUrls(config, options.serviceUrls);
127
+ let lazyAuth = options.auth ?? null;
128
+ const getAuth = () => {
129
+ if (lazyAuth) return lazyAuth;
130
+ const dbUrl = options.databaseUrl ?? process.env.DATABASE_URL;
131
+ if (!dbUrl) return null;
132
+ const dbInstance = createDb({ databaseUrl: dbUrl, schema: authTables });
133
+ lazyAuth = createPandAuth({ db: dbInstance.db, config });
134
+ return lazyAuth;
135
+ };
136
+ const resolveSession = options.resolveSession ?? (async (req) => {
137
+ const auth = getAuth();
138
+ if (auth) {
139
+ return await resolveRequestActor(auth, req);
140
+ }
141
+ return await defaultResolveSession(req);
142
+ });
143
+ const authHandler = options.authHandler ?? (async (req) => {
144
+ const auth = getAuth();
145
+ if (!auth) {
146
+ throw PandError.notFound("Auth handler is not configured on this gateway");
147
+ }
148
+ return await auth.handler(req);
149
+ });
150
+ const logger = createLogger({
151
+ service: "gateway",
152
+ project: config.name,
153
+ level: process.env.NODE_ENV === "test" ? "silent" : void 0
154
+ });
155
+ const app = new Hono();
156
+ app.use("*", async (c, next) => {
157
+ const reqId = c.req.header("x-request-id") ?? crypto.randomUUID();
158
+ c.set("requestId", reqId);
159
+ c.header("x-request-id", reqId);
160
+ await next();
161
+ });
162
+ app.onError((err, c) => {
163
+ const requestId = c.get("requestId") ?? crypto.randomUUID();
164
+ if (err instanceof PandError) {
165
+ return c.json(
166
+ {
167
+ error: {
168
+ code: err.code,
169
+ message: err.message,
170
+ requestId,
171
+ ...err.details ? { details: err.details } : {}
172
+ }
173
+ },
174
+ err.status
175
+ );
176
+ }
177
+ logger.error({ err, requestId }, "gateway_unhandled_error");
178
+ return c.json(
179
+ {
180
+ error: {
181
+ code: "INTERNAL_ERROR",
182
+ message: err instanceof Error ? err.message : "Internal gateway error",
183
+ requestId
184
+ }
185
+ },
186
+ 500
187
+ );
188
+ });
189
+ const callService = async (serviceName, pathAndQuery, init) => {
190
+ const inProcess = options.serviceApps?.[serviceName];
191
+ if (inProcess) {
192
+ const req = new Request(`http://${serviceName}.internal${pathAndQuery}`, init);
193
+ return await inProcess.fetch(req);
194
+ }
195
+ const baseUrl = serviceUrls[serviceName];
196
+ if (!baseUrl) {
197
+ throw PandError.notFound(`Unknown service "${serviceName}"`);
198
+ }
199
+ return await fetch(`${baseUrl.replace(/\/$/, "")}${pathAndQuery}`, init);
200
+ };
201
+ app.get("/healthz", (c) => {
202
+ return c.json({
203
+ status: "ok",
204
+ service: "gateway",
205
+ version: config.pandVersion
206
+ });
207
+ });
208
+ app.get("/readyz", async (c) => {
209
+ const checks = {};
210
+ let ready = true;
211
+ if (options.dbHealth) {
212
+ const dbStatus = await options.dbHealth();
213
+ checks.db = dbStatus;
214
+ if (dbStatus !== "ok") {
215
+ ready = false;
216
+ }
217
+ } else {
218
+ checks.db = "ok";
219
+ }
220
+ return c.json(
221
+ {
222
+ status: ready ? "ready" : "degraded",
223
+ service: "gateway",
224
+ checks
225
+ },
226
+ ready ? 200 : 503
227
+ );
228
+ });
229
+ app.get("/api/config", (c) => {
230
+ const env = process.env.PAND_ENV ?? "dev";
231
+ const ports = resolveProjectPorts(config);
232
+ const posthogKey = process.env.POSTHOG_PUBLIC_KEY;
233
+ const grafanaUrl = env === "prod" ? `https://grafana.${config.domain}` : `http://localhost:${ports.grafanaPort}`;
234
+ const clientConfig = {
235
+ project: config.name,
236
+ displayName: config.displayName,
237
+ env,
238
+ apiBaseUrl: "/api",
239
+ authBaseUrl: "/api/auth",
240
+ ...posthogKey ? {
241
+ posthog: {
242
+ publicKey: posthogKey,
243
+ host: "/ingest"
244
+ }
245
+ } : {},
246
+ links: {
247
+ grafanaUrl,
248
+ posthogUrl: "https://eu.posthog.com",
249
+ apiDocsUrl: "/api/docs"
250
+ }
251
+ };
252
+ return c.json(clientConfig);
253
+ });
254
+ app.all("/api/auth/*", async (c) => {
255
+ return await authHandler(c.req.raw);
256
+ });
257
+ app.get("/api/openapi.json", async (c) => {
258
+ const specs = await Promise.all(
259
+ Object.keys(config.services).map(async (service) => {
260
+ try {
261
+ const res = await callService(service, "/openapi.json");
262
+ if (!res.ok) {
263
+ return { service, spec: { paths: {}, components: { schemas: {} } } };
264
+ }
265
+ const spec = await res.json();
266
+ return { service, spec };
267
+ } catch {
268
+ return { service, spec: { paths: {}, components: { schemas: {} } } };
269
+ }
270
+ })
271
+ );
272
+ const merged = mergeServiceOpenApiSpecs(specs, {
273
+ title: `${config.displayName} API`,
274
+ version: config.pandVersion
275
+ });
276
+ return c.json(merged);
277
+ });
278
+ app.get(
279
+ "/api/docs",
280
+ apiReference({
281
+ spec: {
282
+ url: "/api/openapi.json"
283
+ }
284
+ })
285
+ );
286
+ app.get("/api/admin/meta", async (c) => {
287
+ const actor = await resolveSession(c.req.raw);
288
+ if (!actor) {
289
+ throw PandError.unauthorized("Authentication required");
290
+ }
291
+ if (actor.role !== "admin") {
292
+ throw PandError.forbidden("Admin role required");
293
+ }
294
+ const reqId = c.get("requestId");
295
+ const internalToken = await signInternalToken(
296
+ {
297
+ sub: actor.userId,
298
+ authenticated: true,
299
+ email: actor.email,
300
+ role: actor.role,
301
+ caller: "gateway",
302
+ reqId
303
+ },
304
+ options.internalSecret
305
+ );
306
+ const allMeta = [];
307
+ for (const service of Object.keys(config.services)) {
308
+ try {
309
+ const res = await callService(service, "/_pand/meta", {
310
+ headers: {
311
+ "x-pand-internal-token": internalToken,
312
+ "x-request-id": reqId
313
+ }
314
+ });
315
+ if (res.ok) {
316
+ const body = await res.json();
317
+ const items = Array.isArray(body) ? body : body.entities ?? [];
318
+ allMeta.push(...items);
319
+ }
320
+ } catch (err) {
321
+ logger.warn({ err, service }, "failed_fetching_service_admin_meta");
322
+ }
323
+ }
324
+ return c.json(allMeta);
325
+ });
326
+ const proxyServiceHandler = async (c) => {
327
+ const serviceName = c.req.param("service");
328
+ if (!config.services[serviceName]) {
329
+ throw PandError.notFound(`Service "${serviceName}" not found`);
330
+ }
331
+ const reqId = c.get("requestId");
332
+ const sessionActor = await resolveSession(c.req.raw);
333
+ const internalToken = await signInternalToken(
334
+ sessionActor ? {
335
+ sub: sessionActor.userId,
336
+ authenticated: true,
337
+ email: sessionActor.email,
338
+ role: sessionActor.role,
339
+ caller: "gateway",
340
+ reqId
341
+ } : {
342
+ sub: "anonymous",
343
+ authenticated: false,
344
+ caller: "gateway",
345
+ reqId
346
+ },
347
+ options.internalSecret
348
+ );
349
+ const url = new URL(c.req.url);
350
+ const prefix = `/api/v1/${serviceName}`;
351
+ const strippedPathname = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
352
+ const targetPathAndQuery = `${strippedPathname}${url.search}`;
353
+ const headers = new Headers();
354
+ const contentType = c.req.header("content-type");
355
+ if (contentType) {
356
+ headers.set("content-type", contentType);
357
+ }
358
+ const accept = c.req.header("accept");
359
+ if (accept) {
360
+ headers.set("accept", accept);
361
+ }
362
+ const traceparent = c.req.header("traceparent");
363
+ if (traceparent) {
364
+ headers.set("traceparent", traceparent);
365
+ }
366
+ headers.set("x-request-id", reqId);
367
+ headers.set("x-pand-internal-token", internalToken);
368
+ const method = c.req.method;
369
+ const hasBody = method !== "GET" && method !== "HEAD";
370
+ const bodyBuffer = hasBody ? await c.req.arrayBuffer() : void 0;
371
+ let upstreamRes;
372
+ try {
373
+ upstreamRes = await callService(serviceName, targetPathAndQuery, {
374
+ method,
375
+ headers,
376
+ body: bodyBuffer && bodyBuffer.byteLength > 0 ? bodyBuffer : void 0
377
+ });
378
+ } catch (err) {
379
+ throw PandError.upstream(`Failed to reach service "${serviceName}"`, {
380
+ cause: err instanceof Error ? err.message : String(err)
381
+ });
382
+ }
383
+ const resHeaders = new Headers(upstreamRes.headers);
384
+ resHeaders.set("x-request-id", reqId);
385
+ return new Response(upstreamRes.body, {
386
+ status: upstreamRes.status,
387
+ headers: resHeaders
388
+ });
389
+ };
390
+ app.all("/api/v1/:service", proxyServiceHandler);
391
+ app.all("/api/v1/:service/*", proxyServiceHandler);
392
+ app.all("/ingest/*", async (c) => {
393
+ const url = new URL(c.req.url);
394
+ const subpath = url.pathname.replace(/^\/ingest/, "");
395
+ const isStatic = subpath.startsWith("/static/");
396
+ const targetHost = isStatic ? "https://eu-assets.i.posthog.com" : (process.env.POSTHOG_HOST ?? "https://eu.i.posthog.com").replace(/\/$/, "");
397
+ const targetUrl = `${targetHost}${subpath}${url.search}`;
398
+ const fetchFn = options.posthogFetch ?? fetch;
399
+ const method = c.req.method;
400
+ const hasBody = method !== "GET" && method !== "HEAD";
401
+ const body = hasBody ? await c.req.arrayBuffer() : void 0;
402
+ const headers = new Headers();
403
+ const contentType = c.req.header("content-type");
404
+ if (contentType) {
405
+ headers.set("content-type", contentType);
406
+ }
407
+ const upstream = await fetchFn(targetUrl, {
408
+ method,
409
+ headers,
410
+ body: body && body.byteLength > 0 ? body : void 0
411
+ });
412
+ return new Response(upstream.body, {
413
+ status: upstream.status,
414
+ headers: {
415
+ "content-type": upstream.headers.get("content-type") ?? "application/json"
416
+ }
417
+ });
418
+ });
419
+ app.notFound((c) => {
420
+ if (process.env.PAND_SERVE_STATIC === "true" || options.staticRootDir) {
421
+ const host = c.req.header("host") ?? "";
422
+ const isAdminHost = host.startsWith(`${config.name}-admin.`);
423
+ const baseStatic = options.staticRootDir ?? path.resolve(process.cwd(), "public", isAdminHost ? "admin" : "web");
424
+ const reqPath = c.req.path === "/" ? "/index.html" : c.req.path;
425
+ const candidate = path.join(baseStatic, reqPath);
426
+ const fallback = path.join(baseStatic, "index.html");
427
+ const fileToServe = fs.existsSync(candidate) && fs.statSync(candidate).isFile() ? candidate : fallback;
428
+ if (fs.existsSync(fileToServe)) {
429
+ const ext = path.extname(fileToServe);
430
+ const mimeMap = {
431
+ ".html": "text/html; charset=utf-8",
432
+ ".js": "application/javascript; charset=utf-8",
433
+ ".css": "text/css; charset=utf-8",
434
+ ".json": "application/json; charset=utf-8",
435
+ ".svg": "image/svg+xml",
436
+ ".png": "image/png"
437
+ };
438
+ return new Response(fs.readFileSync(fileToServe), {
439
+ status: 200,
440
+ headers: { "content-type": mimeMap[ext] ?? "application/octet-stream" }
441
+ });
442
+ }
443
+ }
444
+ const requestId = c.get("requestId") ?? crypto.randomUUID();
445
+ return c.json(
446
+ {
447
+ error: {
448
+ code: "NOT_FOUND",
449
+ message: `Route ${c.req.method} ${c.req.path} not found`,
450
+ requestId
451
+ }
452
+ },
453
+ 404
454
+ );
455
+ });
456
+ return Object.assign(app, {
457
+ serviceName: "gateway",
458
+ logger,
459
+ registerAdminEntity: () => {
460
+ },
461
+ getAdminEntities: () => []
462
+ });
463
+ }
464
+ export {
465
+ createGatewayApp,
466
+ mergeServiceOpenApiSpecs,
467
+ resolveGatewayServiceUrls,
468
+ serveService
469
+ };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@pands/gateway",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@hono/node-server": "^1.13.8",
16
+ "@scalar/hono-api-reference": "^0.5.170",
17
+ "hono": "^4.7.2",
18
+ "@pands/config": "0.1.1",
19
+ "@pands/service": "0.1.1",
20
+ "@pands/db": "0.1.1",
21
+ "@pands/telemetry": "0.1.1",
22
+ "@pands/auth": "0.1.1",
23
+ "@pands/logger": "0.1.1"
24
+ },
25
+ "devDependencies": {
26
+ "@pands/testing": "0.1.1"
27
+ },
28
+ "description": "Reverse proxy, routing, and Better Auth authentication gateway for Pand",
29
+ "license": "MIT",
30
+ "author": "Mehdi Nosrati",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/MehdiNosrati/pand.git"
34
+ },
35
+ "homepage": "https://github.com/MehdiNosrati/pand#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/MehdiNosrati/pand/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsup src/index.ts --format esm --dts --clean",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run"
49
+ }
6
50
  }