main(init): Initial commit.

This commit is contained in:
Hellings 2026-09-11 23:30:20 -04:00
commit 71eabae6f9
21 changed files with 643 additions and 0 deletions

26
README.md Normal file
View 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
View file

@ -0,0 +1,2 @@
export declare class AppModule {
}

22
dist/app.module.js vendored Normal file
View 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
View 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
View file

@ -0,0 +1 @@
import 'reflect-metadata';

29
dist/main.js vendored Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
{"version":3,"file":"mount.controller.js","sourceRoot":"","sources":["../src/mount.controller.ts"],"names":[],"mappings":"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"}

20
dist/mount.service.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

File diff suppressed because one or more lines are too long

21
tsconfig.json Normal file
View 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"]
}
}