main(init): Initial commit.
This commit is contained in:
commit
71eabae6f9
21 changed files with 643 additions and 0 deletions
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# mindmount
|
||||
|
||||
Unified router on **:3000**. Single authentication source for the network:
|
||||
|
||||
- `/ips/*` → gapmind (`GAPMIND_URL`, default `http://127.0.0.1:3002`)
|
||||
- `/socials/*` → socmind (`SOCMIND_URL`, default `http://127.0.0.1:3001`)
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
curl localhost:3000/ips/api/resolve?ip=8.8.8.8
|
||||
curl localhost:3000/socials/api/social/search?username=nasa
|
||||
curl localhost:3000/ips/api/docs # gapmind Swagger (proxied)
|
||||
curl localhost:3000/socials/api/docs # socmind Swagger (proxied)
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
| Env | Purpose |
|
||||
|---|---|
|
||||
| `MOUNT_API_TOKEN` | Client-facing token. If set, every proxied request needs `Authorization: Bearer <token>`. If unset, open. |
|
||||
| `GAPMIND_TOKEN` | Shared/configured gapmind service token. Wins over auto-signup. |
|
||||
| `GAPMIND_EMAIL` / `GAPMIND_PASSWORD` | Auto-signup identity (`mindmount@local` default). On boot, mindmount `POST`s gapmind `/api/accounts/signup` and stores the token. |
|
||||
|
||||
Per request: a caller-supplied `Authorization` header is forwarded as-is;
|
||||
otherwise the provisioned gapmind token is injected on `/ips/*` routes.
|
||||
2
dist/app.module.d.ts
vendored
Normal file
2
dist/app.module.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export declare class AppModule {
|
||||
}
|
||||
22
dist/app.module.js
vendored
Normal file
22
dist/app.module.js
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppModule = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const mount_controller_1 = require("./mount.controller");
|
||||
const mount_service_1 = require("./mount.service");
|
||||
let AppModule = class AppModule {
|
||||
};
|
||||
exports.AppModule = AppModule;
|
||||
exports.AppModule = AppModule = __decorate([
|
||||
(0, common_1.Module)({
|
||||
controllers: [mount_controller_1.MountController],
|
||||
providers: [mount_service_1.MountService],
|
||||
})
|
||||
], AppModule);
|
||||
//# sourceMappingURL=app.module.js.map
|
||||
1
dist/app.module.js.map
vendored
Normal file
1
dist/app.module.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yDAAqD;AACrD,mDAA+C;AAMxC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAJrB,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,kCAAe,CAAC;QAC9B,SAAS,EAAE,CAAC,4BAAY,CAAC;KAC1B,CAAC;GACW,SAAS,CAAG"}
|
||||
1
dist/main.d.ts
vendored
Normal file
1
dist/main.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
import 'reflect-metadata';
|
||||
29
dist/main.js
vendored
Normal file
29
dist/main.js
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
require("reflect-metadata");
|
||||
const core_1 = require("@nestjs/core");
|
||||
const swagger_1 = require("@nestjs/swagger");
|
||||
const app_module_1 = require("./app.module");
|
||||
async function bootstrap() {
|
||||
const app = await core_1.NestFactory.create(app_module_1.AppModule);
|
||||
app.enableShutdownHooks();
|
||||
app.enableCors();
|
||||
const config = new swagger_1.DocumentBuilder()
|
||||
.setTitle('mindmount')
|
||||
.setDescription('MindMount is a proxy service for GapMind and SocMind. It\'s a unified API gateway for IP and social intelligence services.')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
.addTag('Mount', 'Router status')
|
||||
.build();
|
||||
const document = swagger_1.SwaggerModule.createDocument(app, config);
|
||||
swagger_1.SwaggerModule.setup('/', app, document);
|
||||
app.getHttpServer().on('request', (req, res) => {
|
||||
if (req.url === '/api/openapi.json' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(document));
|
||||
}
|
||||
});
|
||||
await app.listen(process.env.PORT || 3000);
|
||||
}
|
||||
bootstrap();
|
||||
//# sourceMappingURL=main.js.map
|
||||
1
dist/main.js.map
vendored
Normal file
1
dist/main.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,4BAA0B;AAC1B,uCAA2C;AAE3C,6CAAiE;AACjE,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAyB,sBAAS,CAAC,CAAC;IACxE,GAAG,CAAC,mBAAmB,EAAE,CAAC;IAC1B,GAAG,CAAC,UAAU,EAAE,CAAC;IAEjB,MAAM,MAAM,GAAG,IAAI,yBAAe,EAAE;SACjC,QAAQ,CAAC,WAAW,CAAC;SACrB,cAAc,CACb,4HAA4H,CAC7H;SACA,UAAU,CAAC,OAAO,CAAC;SACnB,aAAa,EAAE;SACf,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC;SAChC,KAAK,EAAE,CAAC;IAEX,MAAM,QAAQ,GAAG,uBAAa,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC3D,uBAAa,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IAExC,GAAG,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAQ,EAAE,GAAQ,EAAE,EAAE;QACvD,IAAI,GAAG,CAAC,GAAG,KAAK,mBAAmB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC5D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAC7C,CAAC;AACD,SAAS,EAAE,CAAC"}
|
||||
22
dist/mount.controller.d.ts
vendored
Normal file
22
dist/mount.controller.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { MountService } from './mount.service';
|
||||
export declare class MountController {
|
||||
private readonly mount;
|
||||
constructor(mount: MountService);
|
||||
health(): {
|
||||
ok: boolean;
|
||||
service: string;
|
||||
routes: {
|
||||
'/ips/{*path}': string;
|
||||
'/socials/{*path}': string;
|
||||
};
|
||||
gapmindTokenProvisioned: boolean;
|
||||
};
|
||||
routes(): {
|
||||
prefix: string;
|
||||
target: string;
|
||||
auth: string;
|
||||
}[];
|
||||
ips(req: any, res: any, auth?: string): Promise<any>;
|
||||
socials(req: any, res: any, auth?: string): Promise<any>;
|
||||
private forward;
|
||||
}
|
||||
112
dist/mount.controller.js
vendored
Normal file
112
dist/mount.controller.js
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||
return function (target, key) { decorator(target, key, paramIndex); }
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MountController = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const swagger_1 = require("@nestjs/swagger");
|
||||
const mount_service_1 = require("./mount.service");
|
||||
let MountController = class MountController {
|
||||
mount;
|
||||
constructor(mount) {
|
||||
this.mount = mount;
|
||||
}
|
||||
health() {
|
||||
return {
|
||||
ok: true,
|
||||
service: 'mindmount',
|
||||
routes: {
|
||||
'/ips/{*path}': this.mount.gapmindBase() + '/*',
|
||||
'/socials/{*path}': this.mount.socmindBase() + '/*',
|
||||
},
|
||||
gapmindTokenProvisioned: Boolean(this.mount.serviceToken()),
|
||||
};
|
||||
}
|
||||
routes() {
|
||||
return [
|
||||
{ prefix: '/ips/{*path}', target: this.mount.gapmindBase(), auth: 'mount token, gapmind service token injected' },
|
||||
{ prefix: '/socials/{*path}', target: this.mount.socmindBase(), auth: 'mount token' },
|
||||
];
|
||||
}
|
||||
async ips(req, res, auth) {
|
||||
return this.forward(req, res, auth, this.mount.gapmindBase(), '/ips', true);
|
||||
}
|
||||
async socials(req, res, auth) {
|
||||
return this.forward(req, res, auth, this.mount.socmindBase(), '/socials', false);
|
||||
}
|
||||
async forward(req, res, auth, base, prefix, injectToken) {
|
||||
try {
|
||||
this.mount.requireMountAuth(auth);
|
||||
}
|
||||
catch (e) {
|
||||
return res.status(e.status || 401).send({ message: e.message || 'Unauthorized' });
|
||||
}
|
||||
const rawUrl = req.raw?.url || req.url || '/';
|
||||
const [path, query] = rawUrl.split('?');
|
||||
const subpath = path.slice(prefix.length) || '/';
|
||||
try {
|
||||
const out = await this.mount.proxy(base, subpath, req.method, query || '', (req.headers || {}), req.body, injectToken);
|
||||
if (out.contentType)
|
||||
res.header('Content-Type', out.contentType);
|
||||
return res.status(out.status).send(out.buffer);
|
||||
}
|
||||
catch (e) {
|
||||
return res
|
||||
.status(502)
|
||||
.send({ message: 'Upstream unreachable', detail: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.MountController = MountController;
|
||||
__decorate([
|
||||
(0, common_1.Get)('health'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'Health + upstream targets' }),
|
||||
(0, swagger_1.ApiResponse)({ status: 200, description: 'Mount status' }),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", void 0)
|
||||
], MountController.prototype, "health", null);
|
||||
__decorate([
|
||||
(0, common_1.Get)('api/routes'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'List proxied routes' }),
|
||||
(0, swagger_1.ApiResponse)({ status: 200, description: 'Route table' }),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", void 0)
|
||||
], MountController.prototype, "routes", null);
|
||||
__decorate([
|
||||
(0, common_1.All)('ips/{*path}'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'Proxy to gapmind (IP intelligence)' }),
|
||||
__param(0, (0, common_1.Req)()),
|
||||
__param(1, (0, common_1.Res)()),
|
||||
__param(2, (0, common_1.Headers)('authorization')),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Object, Object, String]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], MountController.prototype, "ips", null);
|
||||
__decorate([
|
||||
(0, common_1.All)('socials/{*path}'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'Proxy to socmind (social intelligence)' }),
|
||||
__param(0, (0, common_1.Req)()),
|
||||
__param(1, (0, common_1.Res)()),
|
||||
__param(2, (0, common_1.Headers)('authorization')),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Object, Object, String]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], MountController.prototype, "socials", null);
|
||||
exports.MountController = MountController = __decorate([
|
||||
(0, swagger_1.ApiTags)('Mount'),
|
||||
(0, common_1.Controller)(),
|
||||
__metadata("design:paramtypes", [mount_service_1.MountService])
|
||||
], MountController);
|
||||
//# sourceMappingURL=mount.controller.js.map
|
||||
1
dist/mount.controller.js.map
vendored
Normal file
1
dist/mount.controller.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"mount.controller.js","sourceRoot":"","sources":["../src/mount.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAyE;AACzE,6CAAqE;AACrE,mDAA+C;AAIxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAKpD,MAAM;QACJ,OAAO;YACL,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,WAAW;YACpB,MAAM,EAAE;gBACN,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,IAAI;gBAC/C,kBAAkB,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,IAAI;aACpD;YACD,uBAAuB,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;SAC5D,CAAC;IACJ,CAAC;IAKD,MAAM;QACJ,OAAO;YACL,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,6CAA6C,EAAE;YACjH,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;SACtF,CAAC;IACJ,CAAC;IAIK,AAAN,KAAK,CAAC,GAAG,CAAQ,GAAQ,EAAS,GAAQ,EAA4B,IAAa;QACjF,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAC9E,CAAC;IAIK,AAAN,KAAK,CAAC,OAAO,CAAQ,GAAQ,EAAS,GAAQ,EAA4B,IAAa;QACrF,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;IACnF,CAAC;IAEO,KAAK,CAAC,OAAO,CACnB,GAAQ,EACR,GAAQ,EACR,IAAwB,EACxB,IAAY,EACZ,MAAc,EACd,WAAoB;QAEpB,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,MAAM,GAAW,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;QACtD,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAChC,IAAI,EACJ,OAAO,EACP,GAAG,CAAC,MAAM,EACV,KAAK,IAAI,EAAE,EACX,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAA2B,EAC7C,GAAG,CAAC,IAAI,EACR,WAAW,CACZ,CAAC;YACF,IAAI,GAAG,CAAC,WAAW;gBAAE,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;YACjE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,GAAG;iBACP,MAAM,CAAC,GAAG,CAAC;iBACX,IAAI,CAAC,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;IACH,CAAC;CACF,CAAA;AA1EY,0CAAe;AAM1B;IAHC,IAAA,YAAG,EAAC,QAAQ,CAAC;IACb,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC;IACtD,IAAA,qBAAW,EAAC,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;;;;6CAWzD;AAKD;IAHC,IAAA,YAAG,EAAC,YAAY,CAAC;IACjB,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAChD,IAAA,qBAAW,EAAC,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;;;;6CAMxD;AAIK;IAFL,IAAA,YAAG,EAAC,aAAa,CAAC;IAClB,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC;IACrD,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,gBAAO,EAAC,eAAe,CAAC,CAAA;;;;0CAEpE;AAIK;IAFL,IAAA,YAAG,EAAC,iBAAiB,CAAC;IACtB,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAC;IACrD,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,gBAAO,EAAC,eAAe,CAAC,CAAA;;;;8CAExE;0BAtCU,eAAe;IAF3B,IAAA,iBAAO,EAAC,OAAO,CAAC;IAChB,IAAA,mBAAU,GAAE;qCAEyB,4BAAY;GADrC,eAAe,CA0E3B"}
|
||||
20
dist/mount.service.d.ts
vendored
Normal file
20
dist/mount.service.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { OnModuleInit } from '@nestjs/common';
|
||||
export interface RouteTarget {
|
||||
prefix: string;
|
||||
base: string;
|
||||
injectServiceToken: boolean;
|
||||
}
|
||||
export declare class MountService implements OnModuleInit {
|
||||
private readonly logger;
|
||||
private gapmindToken?;
|
||||
gapmindBase(): string;
|
||||
socmindBase(): string;
|
||||
requireMountAuth(authorization?: string): void;
|
||||
serviceToken(): string | undefined;
|
||||
onModuleInit(): Promise<void>;
|
||||
proxy(base: string, subpath: string, method: string, query: string, inHeaders: Record<string, string | string[] | undefined>, body: unknown, injectToken: boolean): Promise<{
|
||||
status: number;
|
||||
contentType?: string;
|
||||
buffer: Buffer;
|
||||
}>;
|
||||
}
|
||||
106
dist/mount.service.js
vendored
Normal file
106
dist/mount.service.js
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var MountService_1;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MountService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
let MountService = MountService_1 = class MountService {
|
||||
logger = new common_1.Logger(MountService_1.name);
|
||||
gapmindToken;
|
||||
gapmindBase() {
|
||||
return (process.env.GAPMIND_URL || 'http://127.0.0.1:3002').replace(/\/$/, '');
|
||||
}
|
||||
socmindBase() {
|
||||
return (process.env.SOCMIND_URL || 'http://127.0.0.1:3001').replace(/\/$/, '');
|
||||
}
|
||||
requireMountAuth(authorization) {
|
||||
const expected = process.env.MOUNT_API_TOKEN;
|
||||
if (!expected)
|
||||
return;
|
||||
if (authorization !== `Bearer ${expected}`) {
|
||||
const err = new Error('Invalid mount credential.');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
serviceToken() {
|
||||
return (this.gapmindToken ||
|
||||
process.env.GAPMIND_TOKEN ||
|
||||
undefined);
|
||||
}
|
||||
async onModuleInit() {
|
||||
if (process.env.GAPMIND_TOKEN) {
|
||||
this.logger.log('Using configured GAPMIND_TOKEN, skipping auto-signup.');
|
||||
return;
|
||||
}
|
||||
const email = process.env.GAPMIND_EMAIL || 'mindmount@local';
|
||||
const password = process.env.GAPMIND_PASSWORD || 'mindmount-local';
|
||||
try {
|
||||
const res = await fetch(`${this.gapmindBase()}/api/accounts/signup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (res.status === 400) {
|
||||
this.logger.log('gapmind account already exists; /ips/* will rely on caller tokens (or set GAPMIND_TOKEN).');
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
this.logger.warn(`gapmind auto-signup failed (${res.status}); /ips/* will rely on caller tokens.`);
|
||||
return;
|
||||
}
|
||||
const data = (await res.json());
|
||||
this.gapmindToken = data.token || data.apiKey || data.apiToken;
|
||||
if (this.gapmindToken)
|
||||
this.logger.log('gapmind instance account provisioned.');
|
||||
else
|
||||
this.logger.warn('gapmind signup returned no token; relying on caller tokens.');
|
||||
}
|
||||
catch (e) {
|
||||
this.logger.warn(`gapmind unreachable at startup (${e instanceof Error ? e.message : e}); /ips/* will rely on caller tokens.`);
|
||||
}
|
||||
}
|
||||
async proxy(base, subpath, method, query, inHeaders, body, injectToken) {
|
||||
const url = `${base}${subpath}${query ? `?${query}` : ''}`;
|
||||
const headers = {};
|
||||
for (const [k, v] of Object.entries(inHeaders)) {
|
||||
const key = k.toLowerCase();
|
||||
if (['host', 'connection', 'content-length', 'transfer-encoding'].includes(key))
|
||||
continue;
|
||||
if (typeof v === 'string')
|
||||
headers[k] = v;
|
||||
else if (Array.isArray(v))
|
||||
headers[k] = v.join(', ');
|
||||
}
|
||||
if (injectToken && !headers['authorization'] && !headers['Authorization']) {
|
||||
const token = this.serviceToken();
|
||||
if (token)
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const hasBody = body !== undefined && body !== null && body !== '' && method !== 'GET' && method !== 'HEAD';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: hasBody ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
||||
});
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
return {
|
||||
status: res.status,
|
||||
contentType: res.headers.get('content-type') || undefined,
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.MountService = MountService;
|
||||
exports.MountService = MountService = MountService_1 = __decorate([
|
||||
(0, common_1.Injectable)()
|
||||
], MountService);
|
||||
//# sourceMappingURL=mount.service.js.map
|
||||
1
dist/mount.service.js.map
vendored
Normal file
1
dist/mount.service.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"mount.service.js","sourceRoot":"","sources":["../src/mount.service.ts"],"names":[],"mappings":"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"}
|
||||
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "mindmount",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified router for gapmind (/ips) and socmind (/socials) with single auth source",
|
||||
"main": "dist/main.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"start": "node dist/main.js",
|
||||
"dev": "NODE_ENV=development tsx watch src/main.ts",
|
||||
"start:production": "NODE_ENV=production node dist/main.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^12.0.1",
|
||||
"@nestjs/core": "^12.0.1",
|
||||
"@nestjs/platform-fastify": "^12.0.1",
|
||||
"@nestjs/swagger": "^12.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.2",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
9
src/app.module.ts
Normal file
9
src/app.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MountController } from './mount.controller';
|
||||
import { MountService } from './mount.service';
|
||||
|
||||
@Module({
|
||||
controllers: [MountController],
|
||||
providers: [MountService],
|
||||
})
|
||||
export class AppModule {}
|
||||
34
src/main.ts
Normal file
34
src/main.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule);
|
||||
app.enableShutdownHooks();
|
||||
app.enableCors();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('mindmount')
|
||||
.setDescription(
|
||||
'MindMount is a proxy service for GapMind and SocMind. It\'s a unified API gateway for IP and social intelligence services.',
|
||||
)
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
.addTag('Mount', 'Router status')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('/', app, document);
|
||||
|
||||
app.getHttpServer().on('request', (req: any, res: any) => {
|
||||
if (req.url === '/api/openapi.json' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(document));
|
||||
}
|
||||
});
|
||||
|
||||
await app.listen(process.env.PORT || 3000);
|
||||
}
|
||||
bootstrap();
|
||||
83
src/mount.controller.ts
Normal file
83
src/mount.controller.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { All, Controller, Get, Headers, Inject, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { MountService } from './mount.service';
|
||||
|
||||
@ApiTags('Mount')
|
||||
@Controller()
|
||||
export class MountController {
|
||||
// NOTE: explicit @Inject — tsx/esbuild strips design:paramtypes metadata,
|
||||
// so implicit constructor injection resolves to undefined under `tsx watch`.
|
||||
constructor(@Inject(MountService) private readonly mount: MountService) {}
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'Health + upstream targets' })
|
||||
@ApiResponse({ status: 200, description: 'Mount status' })
|
||||
health() {
|
||||
return {
|
||||
ok: true,
|
||||
service: 'mindmount',
|
||||
routes: {
|
||||
'/ips/{*path}': this.mount.gapmindBase() + '/*',
|
||||
'/socials/{*path}': this.mount.socmindBase() + '/*',
|
||||
},
|
||||
gapmindTokenProvisioned: Boolean(this.mount.serviceToken()),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('api/routes')
|
||||
@ApiOperation({ summary: 'List proxied routes' })
|
||||
@ApiResponse({ status: 200, description: 'Route table' })
|
||||
routes() {
|
||||
return [
|
||||
{ prefix: '/ips/{*path}', target: this.mount.gapmindBase(), auth: 'mount token, gapmind service token injected' },
|
||||
{ prefix: '/socials/{*path}', target: this.mount.socmindBase(), auth: 'mount token' },
|
||||
];
|
||||
}
|
||||
|
||||
@All('ips/{*path}')
|
||||
@ApiOperation({ summary: 'Proxy to gapmind (IP intelligence)' })
|
||||
async ips(@Req() req: any, @Res() res: any, @Headers('authorization') auth?: string) {
|
||||
return this.forward(req, res, auth, this.mount.gapmindBase(), '/ips', true);
|
||||
}
|
||||
|
||||
@All('socials/{*path}')
|
||||
@ApiOperation({ summary: 'Proxy to socmind (social intelligence)' })
|
||||
async socials(@Req() req: any, @Res() res: any, @Headers('authorization') auth?: string) {
|
||||
return this.forward(req, res, auth, this.mount.socmindBase(), '/socials', false);
|
||||
}
|
||||
|
||||
private async forward(
|
||||
req: any,
|
||||
res: any,
|
||||
auth: string | undefined,
|
||||
base: string,
|
||||
prefix: string,
|
||||
injectToken: boolean,
|
||||
) {
|
||||
try {
|
||||
this.mount.requireMountAuth(auth);
|
||||
} catch (e: any) {
|
||||
return res.status(e.status || 401).send({ message: e.message || 'Unauthorized' });
|
||||
}
|
||||
const rawUrl: string = req.raw?.url || req.url || '/';
|
||||
const [path, query] = rawUrl.split('?');
|
||||
const subpath = path.slice(prefix.length) || '/';
|
||||
try {
|
||||
const out = await this.mount.proxy(
|
||||
base,
|
||||
subpath,
|
||||
req.method,
|
||||
query || '',
|
||||
(req.headers || {}) as Record<string, string>,
|
||||
req.body,
|
||||
injectToken,
|
||||
);
|
||||
if (out.contentType) res.header('Content-Type', out.contentType);
|
||||
return res.status(out.status).send(out.buffer);
|
||||
} catch (e) {
|
||||
return res
|
||||
.status(502)
|
||||
.send({ message: 'Upstream unreachable', detail: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/mount.service.ts
Normal file
115
src/mount.service.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
export interface RouteTarget {
|
||||
prefix: string;
|
||||
base: string;
|
||||
injectServiceToken: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MountService implements OnModuleInit {
|
||||
private readonly logger = new Logger(MountService.name);
|
||||
private gapmindToken?: string;
|
||||
|
||||
gapmindBase(): string {
|
||||
return (process.env.GAPMIND_URL || 'http://127.0.0.1:3002').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
socmindBase(): string {
|
||||
return (process.env.SOCMIND_URL || 'http://127.0.0.1:3001').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
requireMountAuth(authorization?: string): void {
|
||||
const expected = process.env.MOUNT_API_TOKEN;
|
||||
if (!expected) return;
|
||||
if (authorization !== `Bearer ${expected}`) {
|
||||
const err = new Error('Invalid mount credential.') as Error & { status: number };
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
serviceToken(): string | undefined {
|
||||
return (
|
||||
this.gapmindToken ||
|
||||
process.env.GAPMIND_TOKEN ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (process.env.GAPMIND_TOKEN) {
|
||||
this.logger.log('Using configured GAPMIND_TOKEN, skipping auto-signup.');
|
||||
return;
|
||||
}
|
||||
const email = process.env.GAPMIND_EMAIL || 'mindmount@local';
|
||||
const password = process.env.GAPMIND_PASSWORD || 'mindmount-local';
|
||||
try {
|
||||
const res = await fetch(`${this.gapmindBase()}/api/accounts/signup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (res.status === 400) {
|
||||
// Most likely the account already exists (gapmind signup rejects
|
||||
// duplicates with 400). Not an error: fall back to caller tokens.
|
||||
this.logger.log(
|
||||
'gapmind account already exists; /ips/* will rely on caller tokens (or set GAPMIND_TOKEN).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
this.logger.warn(
|
||||
`gapmind auto-signup failed (${res.status}); /ips/* will rely on caller tokens.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as { token?: string; apiKey?: string; apiToken?: string };
|
||||
this.gapmindToken = data.token || data.apiKey || data.apiToken;
|
||||
if (this.gapmindToken) this.logger.log('gapmind instance account provisioned.');
|
||||
else this.logger.warn('gapmind signup returned no token; relying on caller tokens.');
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
`gapmind unreachable at startup (${e instanceof Error ? e.message : e}); /ips/* will rely on caller tokens.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async proxy(
|
||||
base: string,
|
||||
subpath: string,
|
||||
method: string,
|
||||
query: string,
|
||||
inHeaders: Record<string, string | string[] | undefined>,
|
||||
body: unknown,
|
||||
injectToken: boolean,
|
||||
): Promise<{ status: number; contentType?: string; buffer: Buffer }> {
|
||||
const url = `${base}${subpath}${query ? `?${query}` : ''}`;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(inHeaders)) {
|
||||
const key = k.toLowerCase();
|
||||
if (['host', 'connection', 'content-length', 'transfer-encoding'].includes(key)) continue;
|
||||
if (typeof v === 'string') headers[k] = v;
|
||||
else if (Array.isArray(v)) headers[k] = v.join(', ');
|
||||
}
|
||||
if (injectToken && !headers['authorization'] && !headers['Authorization']) {
|
||||
const token = this.serviceToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const hasBody = body !== undefined && body !== null && body !== '' && method !== 'GET' && method !== 'HEAD';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: hasBody ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
||||
});
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
return {
|
||||
status: res.status,
|
||||
contentType: res.headers.get('content-type') || undefined,
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
}
|
||||
8
tsconfig.build.json
Normal file
8
tsconfig.build.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
1
tsconfig.build.tsbuildinfo
Normal file
1
tsconfig.build.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue