Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Minimal DNS server for local test domains -> 127.0.0.1
|
||||
* Works with /etc/resolver/<host> (macOS per-domain DNS override).
|
||||
*/
|
||||
import dgram from 'node:dgram';
|
||||
import { DNS_PORT, LOCAL_IP, TEST_HOSTS } from './local-test-config.mjs';
|
||||
|
||||
const ip = LOCAL_IP.split('.').map(Number);
|
||||
const bind = process.env.LOCAL_TEST_DNS_BIND ?? '127.0.0.1';
|
||||
const hosts = new Set(TEST_HOSTS.flatMap((host) => [host, `${host}.`]));
|
||||
|
||||
function encodeName(name) {
|
||||
const parts = name.split('.').filter(Boolean);
|
||||
return Buffer.concat([
|
||||
...parts.map((part) => Buffer.concat([Buffer.from([part.length]), Buffer.from(part, 'ascii')])),
|
||||
Buffer.from([0]),
|
||||
]);
|
||||
}
|
||||
|
||||
function readQuestionName(msg, offset) {
|
||||
const labels = [];
|
||||
let pos = offset;
|
||||
while (pos < msg.length) {
|
||||
const len = msg[pos];
|
||||
if (len === 0) {
|
||||
pos += 1;
|
||||
break;
|
||||
}
|
||||
labels.push(msg.subarray(pos + 1, pos + 1 + len).toString('ascii'));
|
||||
pos += 1 + len;
|
||||
}
|
||||
return { name: labels.join('.'), next: pos };
|
||||
}
|
||||
|
||||
function questionMatches(name) {
|
||||
return hosts.has(name);
|
||||
}
|
||||
|
||||
const server = dgram.createSocket('udp4');
|
||||
|
||||
server.on('message', (msg, rinfo) => {
|
||||
if (msg.length < 12) return;
|
||||
|
||||
const qdCount = msg.readUInt16BE(4);
|
||||
if (qdCount !== 1) return;
|
||||
|
||||
const question = readQuestionName(msg, 12);
|
||||
const qtype = msg.readUInt16BE(question.next);
|
||||
const qclass = msg.readUInt16BE(question.next + 2);
|
||||
const questionEnd = question.next + 4;
|
||||
|
||||
if (!questionMatches(question.name) || (qtype !== 1 && qtype !== 28) || qclass !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const questionSection = msg.subarray(12, questionEnd);
|
||||
const ttl = Buffer.from([0x00, 0x00, 0x00, 0x3c]);
|
||||
|
||||
let answer;
|
||||
if (qtype === 1) {
|
||||
answer = Buffer.concat([
|
||||
Buffer.from([0xc0, 0x0c]),
|
||||
Buffer.from([0x00, 0x01, 0x00, 0x01]),
|
||||
ttl,
|
||||
Buffer.from([0x00, 0x04]),
|
||||
Buffer.from(ip),
|
||||
]);
|
||||
} else {
|
||||
answer = Buffer.concat([
|
||||
Buffer.from([0xc0, 0x0c]),
|
||||
Buffer.from([0x00, 0x1c, 0x00, 0x01]),
|
||||
ttl,
|
||||
Buffer.from([0x00, 0x10]),
|
||||
Buffer.from([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, ip[0], ip[1], ip[2], ip[3],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
const header = Buffer.alloc(12);
|
||||
header.writeUInt16BE(msg.readUInt16BE(0), 0);
|
||||
header.writeUInt16BE(0x8180, 2);
|
||||
header.writeUInt16BE(1, 4);
|
||||
header.writeUInt16BE(1, 6);
|
||||
header.writeUInt16BE(0, 8);
|
||||
header.writeUInt16BE(0, 10);
|
||||
|
||||
server.send(Buffer.concat([header, questionSection, answer]), rinfo.port, rinfo.address);
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Local test DNS 端口 ${DNS_PORT} 已被占用`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
server.bind(DNS_PORT, bind, () => {
|
||||
console.log(`Local test DNS ${TEST_HOSTS.join(', ')} -> ${LOCAL_IP} @ ${bind}:${DNS_PORT}`);
|
||||
});
|
||||
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||
process.on(signal, () => {
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user