Files
JiboExperiments/OpenJibo/src/Jibo.Cloud/node/open-jibo-link.js
2026-04-09 21:30:23 -05:00

2978 lines
86 KiB
JavaScript

"use strict";
console.log("SERVER_VERSION", "2026-04-04-jibo-fake-cloud-v8-STT");
const os = require("os");
const { spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const https = require("https");
const tls = require("tls");
const crypto = require("crypto");
const { WebSocketServer, WebSocket } = require("ws");
const HOST = "0.0.0.0";
const PORT = 443;
const key = fs.readFileSync("./key.pem");
const cert = fs.readFileSync("./cert.pem");
const LOG_DIR = path.join(__dirname, "logs");
fs.mkdirSync(LOG_DIR, { recursive: true });
const state = {
tokens: new Map(),
hubTokens: new Map(),
hubSessions: new Map(),
symmetricKeys: new Map(),
requests: new Map(),
account: {
id: "usr_test_001",
email: "jibo@example.com",
firstName: "Jibo",
lastName: "Owner",
gender: "unknown",
birthday: 631152000000,
phoneNumber: "+10000000000",
photoUrl: "",
isActive: true,
messagingAllowed: true,
accessKeyId: "fake-access-key-id",
secretAccessKey: "fake-secret-access-key",
roles: [],
facebookConnected: false,
termsAccepted: true
},
robot: {
id: "my-robot-name",
payload: {}
},
robotCreated: null,
loops: [
{
id: "fake-loop-id",
name: "OpenJibo Test Loop",
owner: "usr_test_001",
robot: "my-robot-name",
robotFriendlyId: "my-robot-serial-number",
members: [],
isSuspended: false,
created: 1775099000000,
updated: 1775099000000
}
],
media: [],
updates: []
};
const SSID = "my-ssid";
const TLS_DEBUG = false;
const HTTP_BODY_CONSOLE_LIMIT = 1200;
const WS_TEXT_CONSOLE_LIMIT = 4000;
const WS_BINARY_HEX_PREVIEW_BYTES = 128;
const LOG_BINARY_UPLOAD_PREVIEW = true;
const MAX_HUB_SESSIONS = 250;
const JOKES = [
"Why did the robot cross the road? Because it was programmed by the chicken.",
"Why was the robot tired when it got home? It had a hard drive.",
"What do you call a pirate robot? Arrrr two dee two.",
"Why did the robot go on vacation? It needed to recharge.",
"What is a robots favorite kind of music? Heavy metal.",
"Why did the robot blush? Because it saw the computers motherboard.",
"What do you call a robot that takes the long way around? R two detour.",
"Why did the robot bring a ladder? Because it wanted to reach the cloud.",
"What do robots eat for snacks? Microchips.",
"Why are robots bad at secrets? Because they always leak data.",
"Why dont some couples go to the gym? Because some relationships dont work out.",
"Why did the coffee file a police report? Because it got mugged!",
"What did the grape say when it got stepped on? Nothing, it just let out a little wine.",
"Why did the astronaut break up with his girlfriend? He needed space.",
"Why was the math book sad? Because it had too many problems.",
"Why did the scarecrow win an award? Because he was outstanding in his field.",
"Why dont eggs tell jokes? Theyd crack each other up.",
"What do you call a can<break size=\"0.5\" /> opener that doesnt work? A cant opener.",
"Why did the computer go to the doctor? It had a virus.",
"Why did the kid bring a ladder to school? He wanted to reach his full potential.",
"What do you call a dog that does magic tricks? A labracadabrador.",
"Why did the chicken cross the playground? To get to the other slide.",
"Why did the bicycle fall over? Because it was two-tired.",
"What do you call a pile of cats? A meow-n-tin.",
"What kind of shoes do frogs wear? Open-toed."
];
const ASR_ENABLED = true;
const ASR_MAX_TURN_MS = 1800;
const ASR_MIN_AUDIO_FRAMES = 5;
const ASR_MIN_AUDIO_BYTES = 12000;
const ASR_FINALIZE_GRACE_MS = 1200;
const ASR_FINALIZE_POLL_MS = 250;
const ASR_MIN_GOOD_WAV_BYTES = 2000;
const ASR_EARLY_START_MS = 900;
const ASR_EARLY_MIN_FRAMES = 4;
const ASR_EARLY_MIN_BYTES = 14000;
const ASR_DEBUG_AUDIO = false;
const ASR_DEBUG_OGG = false;
// External tools
const FFMPEG_BIN = process.env.FFMPEG_BIN || "/usr/bin/ffmpeg";
const WHISPER_CPP_BIN = process.env.WHISPER_CPP_BIN || "/usr/bin/whisper.cpp/build/bin/whisper-cli";
const WHISPER_MODEL = process.env.WHISPER_MODEL || "/usr/bin/whisper.cpp/models/ggml-base.en.bin";
// temp storage
const ASR_TMP_DIR = path.join(os.tmpdir(), "openjibo-asr");
fs.mkdirSync(ASR_TMP_DIR, { recursive: true });
function logTiming(event, extra = {}) {
const payload = {
at: nowIso(),
event,
...extra
};
console.log("TIMING", JSON.stringify(payload));
writeStructuredLog(`timing_${event}`, payload);
}
function randomItem(items) {
if (!Array.isArray(items) || items.length === 0) return "";
return items[Math.floor(Math.random() * items.length)];
}
function escapeXml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function sendWsJson(ws, payload, logPrefix = null) {
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
if (logPrefix) {
writeStructuredLog(logPrefix, {
at: nowIso(),
payload
});
}
ws.send(JSON.stringify(payload));
return true;
}
function nowIso() {
return new Date().toISOString();
}
function makeReqId() {
return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
}
function safeJsonParse(buf) {
try {
return JSON.parse(buf.toString("utf8"));
} catch {
return null;
}
}
function sanitizeForFilename(value) {
return String(value || "unknown").replace(/[^a-zA-Z0-9._-]+/g, "_");
}
function writeStructuredLog(prefix, payload) {
try {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const name = `${stamp}_${sanitizeForFilename(prefix)}.json`;
fs.writeFileSync(
path.join(LOG_DIR, name),
JSON.stringify(payload, null, 2),
"utf8"
);
} catch (err) {
console.error("Failed to write structured log:", err);
}
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
});
}
function getTargetParts(req) {
const raw = req.headers["x-amz-target"] || "";
const parts = raw.split(".");
return {
raw,
servicePrefix: parts[0] || "",
operation: parts[1] || ""
};
}
function getHost(req) {
return (req.headers.host || "").split(":")[0].toLowerCase();
}
function summarizeUtf8(value, limit = HTTP_BODY_CONSOLE_LIMIT) {
if (!value) return "";
if (value.length <= limit) return value;
return `${value.slice(0, limit)}\n...[truncated ${value.length - limit} chars]`;
}
function hexPreview(buffer, maxBytes = 64) {
if (!buffer || !buffer.length) return "";
return buffer.subarray(0, Math.min(buffer.length, maxBytes)).toString("hex");
}
function looksMostlyText(buffer) {
if (!buffer || !buffer.length) return true;
let printable = 0;
const sample = buffer.subarray(0, Math.min(buffer.length, 256));
for (const b of sample) {
if (
b === 9 ||
b === 10 ||
b === 13 ||
(b >= 32 && b <= 126)
) {
printable++;
}
}
return printable / sample.length > 0.85;
}
function buildRequestRecord(req, bodyBuffer) {
const parsed = safeJsonParse(bodyBuffer);
const target = getTargetParts(req);
const host = getHost(req);
return {
reqId: makeReqId(),
at: nowIso(),
host,
method: req.method,
url: req.url,
headers: req.headers,
target,
bodyLength: bodyBuffer.length,
bodyLooksText: looksMostlyText(bodyBuffer),
bodyUtf8: bodyBuffer.length ? bodyBuffer.toString("utf8") : "",
bodyHexPreview: bodyBuffer.length ? hexPreview(bodyBuffer, 128) : "",
bodyJson: parsed
};
}
function consoleBanner(record) {
console.log("==== HTTPS REQUEST ====");
console.log("ReqId:", record.reqId);
console.log("Time:", record.at);
console.log("Host:", record.host);
console.log("Method:", record.method);
console.log("URL:", record.url);
console.log("X-Amz-Target:", record.target.raw || "<none>");
console.log("BodyLength:", record.bodyLength);
console.log("Headers:", JSON.stringify(record.headers, null, 2));
if (!record.bodyLength) {
console.log("Body: <empty>");
} else if (record.bodyLooksText) {
console.log("Body (utf8):", summarizeUtf8(record.bodyUtf8));
} else {
console.log("Body: <binary>");
console.log("Body HEX preview:", record.bodyHexPreview);
}
console.log("=======================");
}
function respondPlanned(res, plan) {
if (Object.prototype.hasOwnProperty.call(plan, "rawBody")) {
const body = plan.rawBody || "";
res.writeHead(plan.statusCode, {
"Content-Length": Buffer.byteLength(body),
Connection: "close",
...plan.extraHeaders
});
return res.end(body);
}
const body = JSON.stringify(plan.body ?? {});
res.writeHead(plan.statusCode, {
"Content-Type": "application/x-amz-json-1.1",
"Content-Length": Buffer.byteLength(body),
Connection: "keep-alive",
...plan.extraHeaders
});
res.end(body);
}
function makeToken(deviceId) {
const token = `token-${deviceId}-${Date.now()}`;
state.tokens.set(token, {
deviceId,
accountId: state.account.id,
createdAt: nowIso()
});
return token;
}
function makeHubToken() {
const token = `hub-${state.account.id}-${Date.now()}`;
state.hubTokens.set(token, {
accountId: state.account.id,
robotId: state.robot.id,
createdAt: nowIso()
});
return token;
}
function getDeviceIdFromBody(parsed) {
if (!parsed || typeof parsed !== "object") {
return "unknown-device";
}
return (
parsed.deviceId ||
parsed.serial_number ||
parsed.serialNumber ||
parsed.cpuid ||
parsed.cpuId ||
parsed.robotId ||
"unknown-device"
);
}
function getLoopId(parsed) {
return parsed?.loopId || parsed?.id || state.loops[0].id;
}
function getOrCreateSymmetricKey(loopId) {
if (!state.symmetricKeys.has(loopId)) {
const keyMaterial = Buffer.from(`open-jibo-symmetric-key:${loopId}`).toString("base64");
state.symmetricKeys.set(loopId, keyMaterial);
}
return state.symmetricKeys.get(loopId);
}
function getBearerToken(request) {
const auth = request.headers.authorization || "";
const m = auth.match(/^Bearer\s+(.+)$/i);
return m ? m[1] : null;
}
function classifyWebSocket(request) {
const host = (request.headers.host || "").split(":")[0].toLowerCase();
const url = request.url || "/";
const pathToken = url.replace(/^\//, "");
const bearerToken = getBearerToken(request);
let kind = "unknown";
if (host === "api-socket.jibo.com") {
kind = "api-socket";
} else if (host === "neo-hub.jibo.com") {
kind = url.startsWith("/v1/proactive")
? "neo-hub-proactive"
: "neo-hub-listen";
}
return {
host,
url,
kind,
pathToken,
bearerToken,
pathTokenKnown: state.tokens.has(pathToken),
bearerTokenKnown: bearerToken ? state.hubTokens.has(bearerToken) : false,
transId: request.headers["x-jibo-transid"] || null,
robotId: request.headers["x-jibo-robotid"] || null
};
}
function handleAccountOperation(operation, parsed) {
console.log("ACCOUNT OPERATION:", operation, "BODY:", JSON.stringify(parsed || {}));
if (operation === "CreateHubToken") {
const expires = Date.now() + 60 * 60 * 1000;
return {
statusCode: 200,
note: "Issued CreateHubToken",
body: {
token: makeHubToken(),
expires
}
};
}
if (operation === "CreateAccessToken") {
const expires = Date.now() + 60 * 60 * 1000;
return {
statusCode: 200,
note: "Issued CreateAccessToken",
body: {
token: `access-${state.account.id}-${Date.now()}`,
expires
}
};
}
if (operation === "CheckEmail") {
const email = parsed?.email || "";
return {
statusCode: 200,
note: "Checked email",
body: {
exists: email.toLowerCase() === String(state.account.email).toLowerCase()
}
};
}
if (operation === "Create" || operation === "Login") {
const email = parsed?.email || state.account.email;
const firstName = parsed?.firstName || state.account.firstName;
const lastName = parsed?.lastName || state.account.lastName;
return {
statusCode: 200,
note: `Handled account ${operation}`,
body: {
...state.account,
email,
firstName,
lastName
}
};
}
if (operation === "Get") {
const ids = Array.isArray(parsed?.ids) ? parsed.ids : null;
const matches = !ids || ids.length === 0 || ids.includes(state.account.id);
return {
statusCode: 200,
note: "Returned account list",
body: matches ? [{ ...state.account }] : []
};
}
if (
operation === "Update" ||
operation === "ResetKeys" ||
operation === "Remove" ||
operation === "ActivateByCode" ||
operation === "ResendActivationCode" ||
operation === "ChangePassword" ||
operation === "SendPasswordReset" ||
operation === "PasswordResetByCode" ||
operation === "UpdatePhoto" ||
operation === "RemovePhoto" ||
operation === "VerifyPhoneByCode" ||
operation === "AcceptTerms" ||
operation === "FacebookConnect" ||
operation === "FacebookMobileConnect"
) {
return {
statusCode: 200,
note: `Handled account ${operation}`,
body: {
...state.account
}
};
}
if (operation === "ChangeEmail" || operation === "SendPhoneVerificationCode") {
return {
statusCode: 200,
note: `Handled account ${operation}`,
body: {
id: state.account.id
}
};
}
if (operation === "GetAccountByAccessToken") {
return {
statusCode: 200,
note: "Returned account by access token",
body: {
id: state.account.id,
accessKeyId: state.account.accessKeyId,
secretAccessKey: state.account.secretAccessKey,
email: state.account.email,
friendlyId: state.robot.id,
payload: parsed?.payload || {}
}
};
}
if (operation === "Search") {
const query = String(parsed?.query || "").toLowerCase();
const haystack = [
state.account.email,
state.account.firstName,
state.account.lastName,
state.account.id
].join(" ").toLowerCase();
return {
statusCode: 200,
note: "Returned account search results",
body: query && haystack.includes(query) ? [{ ...state.account }] : []
};
}
if (operation === "FacebookPrepareLogin") {
return {
statusCode: 200,
note: "Prepared Facebook login",
body: {
url: "https://example.com/facebook-login",
client_id: "fake-client-id",
scope: "email",
response_type: "token",
state: `fb-${Date.now()}`,
redirect_uri: "https://api.jibo.com/facebook/callback"
}
};
}
if (operation === "ConfirmEmailReset") {
return {
statusCode: 200,
note: "Confirmed email reset",
body: {}
};
}
return {
statusCode: 200,
note: "Default account handler",
body: {
...state.account
}
};
}
function handleNotificationOperation(operation, parsed) {
if (operation === "NewRobotToken") {
const deviceId = getDeviceIdFromBody(parsed);
const token = makeToken(deviceId);
return {
statusCode: 200,
note: "Issued robot token",
body: { token }
};
}
return {
statusCode: 200,
note: "Default notification handler",
body: { ok: true, operation }
};
}
function handleLoopOperation(operation) {
console.log("LOOP OPERATION:", operation);
if (operation === "List" || operation === "ListLoops") {
return {
statusCode: 200,
note: "Returned one loop",
body: state.loops
};
}
return {
statusCode: 200,
note: "Default loop handler",
body: []
};
}
function handleLogOperation(operation, parsed) {
if (operation === "PutEventsAsync") {
return {
statusCode: 200,
note: "Accepted log events async",
body: {
contentEncoding: "gzip",
uploadUrl: "https://api.jibo.com/upload/log-events"
}
};
}
if (operation === "PutEvents") {
return {
statusCode: 200,
note: "Accepted inline log events",
body: {}
};
}
if (operation === "PutBinaryAsync") {
return {
statusCode: 200,
note: "Accepted binary async request",
body: {
url: "https://api.jibo.com/log/binary/fake-id",
uploadUrl: "https://api.jibo.com/upload/log-binary"
}
};
}
if (operation === "PutAsrBinary") {
logTiming("put_asr_binary_request", {
body: parsed || {}
});
return {
statusCode: 200,
note: "Accepted ASR binary request",
body: {
bucketName: "openjibo-test",
key: "asr/fake-key",
uploadUrl: "https://api.jibo.com/upload/asr-binary"
}
};
}
if (operation === "NewKinesisCredentials") {
return {
statusCode: 200,
note: "Returned fake kinesis credentials",
body: {
credentials: {
AccessKeyId: "fake-access-key",
Expiration: new Date(Date.now() + 3600 * 1000).toISOString(),
SecretAccessKey: "fake-secret",
SessionToken: "fake-session"
},
region: "us-east-1",
streamName: "openjibo-log-stream"
}
};
}
return {
statusCode: 200,
note: "Default log handler",
body: {}
};
}
function handleMediaOperation(operation, parsed, record) {
if (operation === "List") {
const loopIds = Array.isArray(parsed?.loopIds) ? parsed.loopIds : [];
const after = typeof parsed?.after === "number" ? parsed.after : null;
const before = typeof parsed?.before === "number" ? parsed.before : null;
const items = state.media.filter((item) => {
if (loopIds.length && !loopIds.includes(item.loopId)) return false;
if (after != null && !(item.created > after)) return false;
if (before != null && !(item.created < before)) return false;
return true;
});
return {
statusCode: 200,
note: `Returned ${items.length} media items`,
body: items
};
}
if (operation === "Get") {
const paths = Array.isArray(parsed?.paths) ? parsed.paths : [];
const items = state.media.filter((item) => paths.includes(item.path));
return {
statusCode: 200,
note: `Returned ${items.length} requested media items`,
body: items
};
}
if (operation === "Remove") {
const paths = Array.isArray(parsed?.paths) ? parsed.paths : [];
state.media = state.media.map((item) =>
paths.includes(item.path)
? { ...item, isDeleted: true }
: item
);
const items = state.media.filter((item) => paths.includes(item.path));
return {
statusCode: 200,
note: `Marked ${items.length} media items deleted`,
body: items
};
}
if (operation === "Create") {
const created = Date.now();
const loopId =
record.headers["x-loop-id"] ||
parsed?.loopId ||
state.loops[0].id;
const mediaPath =
record.headers["x-path"] ||
parsed?.path ||
`/media/${created}`;
const mediaType =
record.headers["x-type"] ||
parsed?.type ||
"unknown";
const reference =
record.headers["x-reference"] ||
parsed?.reference ||
"";
const encryptedHeader = record.headers["x-encrypted"];
const isEncrypted =
encryptedHeader === true ||
encryptedHeader === "true" ||
parsed?.isEncrypted === true;
let meta = parsed?.meta || {};
// Optional: support x-meta as JSON string if Jibo ever sends it that way
if ((!meta || Object.keys(meta).length === 0) && record.headers["x-meta"]) {
try {
meta = JSON.parse(record.headers["x-meta"]);
} catch {
meta = {};
}
}
const item = {
path: mediaPath,
created,
type: mediaType,
reference,
accountId: state.account.id,
loopId,
url: `https://api.jibo.com/media/${encodeURIComponent(mediaPath)}`,
isEncrypted,
isDeleted: false,
meta
};
state.media.push(item);
return {
statusCode: 200,
note: "Created media item",
body: item
};
}
return {
statusCode: 200,
note: "Default media handler",
body: []
};
}
function handlePersonOperation(operation) {
if (operation === "ListHolidays") {
return {
statusCode: 200,
note: "Returned 1 holidays",
body: [
{
"id": "easter-1",
"eventId": null,
"name": "Easter",
"category": "holiday",
"subcategory": null,
"loopId": state.loops[0].id,
"memberId": null,
"isEnabled": true,
"date": "2026-04-05",
"endDate": null,
"created": nowIso()
}
]
};
}
return {
statusCode: 200,
note: "Default person handler",
body: []
};
}
function handleBackupOperation(operation) {
if (operation === "List") {
return {
statusCode: 200,
note: "No backups available",
body: []
};
}
return {
statusCode: 200,
note: "Default backup handler",
body: []
};
}
function handleKeyOperation(operation, parsed) {
const loopId = getLoopId(parsed);
if (operation === "ShouldCreate") {
return {
statusCode: 200,
note: "Allow local symmetric key creation",
body: { shouldCreate: true }
};
}
if (operation === "CreateSymmetricKey") {
const symmetricKey = getOrCreateSymmetricKey(loopId);
return {
statusCode: 200,
note: "Created symmetric key",
body: {
loopId,
key: symmetricKey,
symmetricKey,
created: true
}
};
}
if (operation === "CreateRequest" || operation === "RequestSymmetricKey") {
const id = `req-${Date.now()}`;
const requestRecord = {
id,
loopId,
publicKey: parsed?.publicKey || "",
encryptedKey: "",
createdAt: nowIso()
};
state.requests.set(id, requestRecord);
return {
statusCode: 200,
note: "Accepted symmetric key request",
body: {
id,
loopId
}
};
}
if (operation === "GetRequest") {
const id = parsed?.id;
const requestRecord = state.requests.get(id) || {
id: id || "unknown-request",
loopId,
publicKey: parsed?.publicKey || "",
encryptedKey: ""
};
return {
statusCode: 200,
note: "Returned request record",
body: requestRecord
};
}
if (operation === "ListIncomingRequests") {
return {
statusCode: 200,
note: "No incoming key requests",
body: []
};
}
if (operation === "ListBinaryRequests") {
return {
statusCode: 200,
note: "No incoming binary requests",
body: []
};
}
if (operation === "Share" || operation === "ShareSymmetricKey") {
return {
statusCode: 200,
note: "Accepted shared symmetric key",
body: { ok: true }
};
}
if (operation === "ShareBinary") {
return {
statusCode: 200,
note: "Accepted shared binary",
body: { ok: true }
};
}
if (operation === "LoadSymmetricKey") {
const symmetricKey = state.symmetricKeys.get(loopId) || "";
return {
statusCode: 200,
note: "Returned cached symmetric key if present",
body: {
loopId,
key: symmetricKey,
symmetricKey
}
};
}
return {
statusCode: 200,
note: "Default key handler",
body: { ok: true, operation }
};
}
function normalizeUpdate(update) {
return {
_id: update._id,
created: update.created,
accountId: update.accountId,
fromVersion: update.fromVersion,
toVersion: update.toVersion,
changes: update.changes,
url: update.url,
shaHash: update.shaHash,
length: update.length ?? 0,
subsystem: update.subsystem,
filter: update.filter ?? null,
dependencies: update.dependencies ?? {}
};
}
function findMatchingUpdates(parsed) {
const fromVersion = parsed?.fromVersion || null;
const subsystem = parsed?.subsystem || null;
const filter = parsed?.filter || null;
return state.updates.filter((u) => {
if (fromVersion && u.fromVersion !== fromVersion) return false;
if (subsystem && u.subsystem !== subsystem) return false;
if (filter && u.filter !== filter) return false;
return true;
});
}
function handleUpdateOperation(operation, parsed) {
console.log("UPDATE OPERATION:", operation, "BODY:", JSON.stringify(parsed || {}));
if (operation === "ListUpdates") {
const subsystem = parsed?.subsystem || null;
const filter = parsed?.filter || null;
const matches = state.updates.filter((u) => {
if (subsystem && u.subsystem !== subsystem) return false;
if (filter && u.filter !== filter) return false;
return true;
});
return {
statusCode: 200,
note: `Returned ${matches.length} updates`,
body: matches.map(normalizeUpdate)
};
}
if (operation === "ListUpdatesFrom") {
const matches = findMatchingUpdates(parsed);
return {
statusCode: 200,
note: `Returned ${matches.length} matching updates`,
body: matches.map(normalizeUpdate)
};
}
if (operation === "GetUpdateFrom") {
const match = findMatchingUpdates(parsed)[0];
if (match) {
return {
statusCode: 200,
note: "Returned matching update",
body: normalizeUpdate(match)
};
}
const fromVersion = parsed?.fromVersion || "unknown";
const subsystem = parsed?.subsystem || "unknown";
const filter = parsed?.filter || null;
const created = Date.now();
return {
statusCode: 200,
note: "Returned placeholder no-op update",
body: {
_id: `noop-update-${subsystem}-${fromVersion}`,
created,
accountId: state.account.id,
fromVersion,
toVersion: fromVersion,
changes: "No update available",
url: "https://api.jibo.com/update/noop",
shaHash: "noop",
length: 0,
subsystem,
filter,
dependencies: {}
}
};
}
if (operation === "CreateUpdate") {
const created = Date.now();
const update = {
_id: `upd-${created}`,
created,
accountId: state.account.id,
fromVersion: parsed?.fromVersion || "unknown",
toVersion: parsed?.toVersion || parsed?.fromVersion || "unknown",
changes: parsed?.changes || "",
url: `https://api.jibo.com/update/upd-${created}`,
shaHash: parsed?.shaHash || "fake-sha-hash",
length: parsed?.length || 0,
subsystem: parsed?.subsystem || "unknown",
filter: parsed?.filter || null,
dependencies: parsed?.dependencies || {}
};
state.updates.push(update);
return {
statusCode: 200,
note: "Created update metadata",
body: normalizeUpdate(update)
};
}
if (operation === "RemoveUpdate") {
const id = parsed?.id;
const idx = state.updates.findIndex((u) => u._id === id);
if (idx >= 0) {
const [removed] = state.updates.splice(idx, 1);
return {
statusCode: 200,
note: "Removed update",
body: normalizeUpdate(removed)
};
}
return {
statusCode: 200,
note: "Update not found; returned placeholder",
body: {
_id: id || "unknown-update",
created: Date.now(),
accountId: state.account.id,
fromVersion: "unknown",
toVersion: "unknown",
changes: "Update not found",
url: "https://api.jibo.com/update/missing",
shaHash: "missing",
length: 0,
subsystem: "unknown",
filter: null,
dependencies: {}
}
};
}
return {
statusCode: 200,
note: "Default update handler",
body: []
};
}
function handleRobotOperation(operation, parsed) {
if (operation === "UpdateRobot") {
return {
statusCode: 200,
note: "Robot updated",
body: {
result: "ok"
}
};
}
if (operation === "GetRobot") {
const id = parsed?.id || state.robot.id;
return {
statusCode: 200,
note: "Returned robot",
body: {
id,
payload: {
SSID,
connectedAt: Date.now(),
platform: "12.10.0",
serialNumber: "my-robot-serial-number"
},
calibrationPayload: {},
updated: Date.now(),
created: state.robotCreated ?? (state.robotCreated = Date.now())
}
};
}
return {
statusCode: 200,
note: "Default robot handler",
body: { result: "ok" }
};
}
function chooseResponse(record) {
const { servicePrefix, operation } = record.target;
const parsed = record.bodyJson || {};
const host = record.host;
if (record.method === "PUT" && record.url === "/upload/asr-binary") {
logTiming("put_asr_binary_upload", {
reqId: record.reqId,
bodyLength: record.bodyLength,
bodyHexPreview: record.bodyHexPreview
});
return {
statusCode: 200,
note: "Accepted uploaded blob",
rawBody: ""
};
}
if (
record.method === "PUT" &&
(
record.url === "/upload/log-events" ||
record.url === "/upload/log-binary"
)
) {
return {
statusCode: 200,
note: "Accepted uploaded blob",
rawBody: ""
};
}
if (record.method === "GET" && record.url === "/" && !record.target.raw) {
return {
statusCode: 204,
note: "Root probe",
body: {}
};
}
if (record.method === "GET" && record.url === "/health") {
return {
statusCode: 200,
note: "Health check",
body: { ok: true, host }
};
}
if (host !== "api.jibo.com") {
console.log("NON-API HOST HIT:", host, record.method, record.url, record.target.raw || "<none>");
writeStructuredLog("non_api_host_hit", {
at: nowIso(),
reqId: record.reqId,
host,
method: record.method,
url: record.url,
target: record.target.raw || "",
headers: record.headers,
bodyLength: record.bodyLength,
bodyUtf8: record.bodyLooksText ? record.bodyUtf8 : "",
bodyHexPreview: record.bodyHexPreview
});
return {
statusCode: 200,
note: "Default catch-all non-api host handler",
body: { ok: true, host, note: "default catch-all handler" }
};
}
if (servicePrefix.startsWith("Log_")) {
return handleLogOperation(operation, parsed);
}
if (servicePrefix.startsWith("Backup_")) {
return handleBackupOperation(operation, parsed);
}
if (servicePrefix.startsWith("Account_")) {
return handleAccountOperation(operation, parsed);
}
if (servicePrefix.startsWith("Notification_")) {
return handleNotificationOperation(operation, parsed);
}
if (servicePrefix.startsWith("Loop_")) {
return handleLoopOperation(operation, parsed);
}
if (servicePrefix === "Media_20160725") {
return handleMediaOperation(operation, parsed, record);
}
if (servicePrefix.startsWith("Key_")) {
return handleKeyOperation(operation, parsed);
}
if (servicePrefix.startsWith("Person_")) {
return handlePersonOperation(operation, parsed);
}
if (servicePrefix.startsWith("Robot_")) {
return handleRobotOperation(operation, parsed);
}
if (servicePrefix.startsWith("Update_")) {
return handleUpdateOperation(operation, parsed);
}
writeStructuredLog("unknown_target", {
at: nowIso(),
reqId: record.reqId,
host,
method: record.method,
url: record.url,
target: record.target.raw,
servicePrefix,
operation,
headers: record.headers,
bodyLength: record.bodyLength,
bodyUtf8: record.bodyLooksText ? record.bodyUtf8 : "",
bodyHexPreview: record.bodyHexPreview,
bodyJson: parsed
});
return {
statusCode: 200,
note: "Unknown target; logged for follow-up",
body: {
ok: true,
host,
target: record.target.raw,
operation,
note: "unknown target default response"
}
};
}
function buildWsContext(request) {
const context = classifyWebSocket(request);
return {
...context,
wsId: `ws-${Date.now()}-${crypto.randomBytes(3).toString("hex")}`
};
}
function logWsConnected(ctx, request) {
console.log("==== WS CONNECTED ====");
console.log("WsId:", ctx.wsId);
console.log("Time:", nowIso());
console.log("Host:", ctx.host);
console.log("Path:", ctx.url);
console.log("WS KIND:", ctx.kind);
if (ctx.kind === "api-socket") {
console.log("Token known:", ctx.pathTokenKnown);
} else {
console.log("Bearer Token:", ctx.bearerToken);
console.log("Bearer Token Known:", ctx.bearerTokenKnown);
console.log("TransId:", ctx.transId);
console.log("RobotId:", ctx.robotId);
}
console.log("Headers:", JSON.stringify(request.headers, null, 2));
console.log("======================");
writeStructuredLog("ws_connected", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
pathTokenKnown: ctx.pathTokenKnown,
bearerTokenKnown: ctx.bearerTokenKnown,
transId: ctx.transId,
robotId: ctx.robotId,
headers: request.headers
});
}
function sendApiSocketGreeting(ws, ctx) {
if (ws.readyState !== WebSocket.OPEN) return;
// Intentionally do nothing.
// The robot opens api-socket successfully without needing our fake greeting,
// and the previous fake message caused NotificationManager to dereference
// message.payload.name and throw.
return;
// if we need to send, send below
ws.send(JSON.stringify({
"payload": {
"name": "StatusConnected",
"payload": {}
}
}));
}
function getOrCreateHubSession(ctx) {
const key = buildHubSessionKey(ctx);
let session = state.hubSessions.get(key);
if (!session) {
session = {
key,
createdAt: nowIso(),
responded: false,
closed: false,
sawListen: false,
sawContext: false,
listenMsg: null,
contextMsg: null,
audioBytes: 0,
audioFrames: 0,
lastAudioAt: null,
lastListenAt: null,
lastContextAt: null,
clientNluMsg: null,
lastClientNluAt: null,
audioChunks: [],
audioFilePath: null,
wavFilePath: null,
wavFilePathOriginal: null,
asrTimer: null,
asrRunning: false,
asrDone: false,
lastTranscript: null,
firstAudioAtMs: null,
lastAudioAtMs: null,
partialResults: [],
bestTranscript: "",
lastNonEmptyTranscript: "",
successfulChunkCount: 0,
processedChunkCount: 0,
finalizing: false,
finalizeStartedAtMs: null,
finalizeDeadlineAtMs: null,
finalizeTimer: null,
lastFinalizeAttemptAtMs: null,
turnStartedAt: Date.now()
};
state.hubSessions.set(key, session);
pruneHubSessions();
}
return session;
}
function makeSessionFileBase(ctx, session) {
const transId = (ctx.transId || "no-trans").replace(/[^a-zA-Z0-9._-]/g, "_");
return path.join(ASR_TMP_DIR, `${Date.now()}_${transId}`);
}
function cleanupSessionAudioFiles(session) {
if (!session) return;
for (const f of [session.audioFilePath, session.wavFilePath, session.wavFilePathOriginal]) {
if (!f) continue;
try { fs.unlinkSync(f); } catch {}
}
session.audioFilePath = null;
session.wavFilePath = null;
session.wavFilePathOriginal = null;
}
function runProcess(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: ["ignore", "pipe", "pipe"],
...options
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => { stdout += d.toString("utf8"); });
child.stderr.on("data", (d) => { stderr += d.toString("utf8"); });
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr, code });
} else {
reject(new Error(`${command} exited with code ${code}\n${stderr}`));
}
});
});
}
async function trimAndBoostWav(inputWavPath, outputWavPath) {
await runProcess(FFMPEG_BIN, [
"-y",
"-i", inputWavPath,
"-af",
"silenceremove=start_periods=1:start_duration=0.08:start_threshold=-42dB:stop_periods=-1:stop_duration=0.60:stop_threshold=-42dB,apad=pad_dur=0.6,volume=6dB",
"-ar", "16000",
"-ac", "1",
"-f", "wav",
outputWavPath
]);
}
async function analyzeWav(wavPath) {
return await runProcess(FFMPEG_BIN, [
"-i", wavPath,
"-af", "volumedetect",
"-f", "null",
"-"
]);
}
async function convertOggToWav(oggPath, wavPath) {
await runProcess(FFMPEG_BIN, [
"-y",
"-i", oggPath,
"-ar", "16000",
"-ac", "1",
"-f", "wav",
wavPath
]);
}
async function transcribeWithWhisperCpp(wavPath) {
const args = [
"-m", WHISPER_MODEL,
"-f", wavPath,
"-l", "en"
];
const result = await runProcess(WHISPER_CPP_BIN, args);
logTiming("whisper_cli_result", {
wavPath,
stdout: result.stdout,
stderr: result.stderr
});
const lines = result.stdout
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean);
const transcriptLines = lines
.filter((line) => /^\[\d{2}:\d{2}:\d{2}\.\d{3}\s+-->/.test(line))
.map((line) => line.replace(/^\[[^\]]+\]\s*/, "").trim())
.filter(Boolean);
return transcriptLines.join(" ").trim();
}
function classifyTranscript(text) {
const heard = String(text || "").trim();
const lower = normalizeTranscriptText(text);
console.log("LOOKING FOR NORMALIZED TEXT", JSON.stringify({
lower
}, null, 2));
if (!heard) {
return { intent: "unknown", replyType: "fallback", heardText: "" };
}
if (/\bjoke\b|funny|make me laugh/.test(lower)) {
return { intent: "joke", replyType: "joke", heardText: heard };
}
if (/\bdate and time\b/.test(lower)) {
return { intent: "date_time", replyType: "chat", heardText: heard };
}
if (lower === "time") {
return { intent: "time", replyType: "chat", heardText: heard };
}
if (/\bwhat time is it\b|\bcurrent time\b|\btime is it\b|\bthe time\b/.test(lower)) {
return { intent: "time", replyType: "chat", heardText: heard };
}
if (lower === "today" ||lower === "day" || lower === "date") {
return { intent: "date", replyType: "chat", heardText: heard };
}
if (/\bwhat day is it\b|\bwhat is the date\b|\btoday'?s date\b|\bdate\b|\bwhat day\b/.test(lower)) {
return { intent: "date", replyType: "chat", heardText: heard };
}
if (/\bdance\b/.test(lower)) {
return { intent: "dance", replyType: "chat", heardText: heard };
}
if (/\bhow are you\b|\bwhats up\b|\bwhat s up\b|\bwhat up\b/.test(lower)) {
return { intent: "how_are_you", replyType: "chat", heardText: heard };
}
if (/\bgood morning\b/.test(lower)) {
return { intent: "good_morning", replyType: "chat", heardText: heard };
}
if (/\bgood afternoon\b/.test(lower)) {
return { intent: "good_afternoon", replyType: "chat", heardText: heard };
}
if (/\bgood night\b|\bnight night\b|\bbed time\b/.test(lower)) {
return { intent: "good_night", replyType: "chat", heardText: heard };
}
if (/\bhello\b|\bhi\b|\bhey\b/.test(lower)) {
return { intent: "hello", replyType: "chat", heardText: heard };
}
if (/\byes\b|\bsure\b|\byeah\b|\byup\b|\buh huh\b/.test(lower)) {
return { intent: "yes", replyType: "chat", heardText: heard };
}
if (/\bno\b|\bnope\b|\bnah\b/.test(lower)) {
return { intent: "no", replyType: "chat", heardText: heard };
}
return { intent: "chat", replyType: "chat", heardText: heard };
}
function isTranscriptUsable(text, session = null) {
const t = String(text || "")
.trim()
.toLowerCase()
.replace(/[^\w\s]/g, "")
.replace(/\s+/g, " ")
.trim();
if (!t) return false;
if ([
"joke",
"dance",
"time",
"date",
"good morning",
"good afternoon",
"good night",
"hello",
"what's up?",
"what up",
"how are you",
"hi",
"hey"
].includes(t)) {
return true;
}
if (isYesNoTurn(session) && ["yes", "no", "sure", "nope", "yup", "uh huh", "yeah", "nah"].includes(t)) {
return true;
}
return t.length >= 6;
}
function normalizeTranscriptText(text) {
return String(text || "")
.trim()
.toLowerCase()
.replace(/[^\w\s]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function getLocalDateTimeParts() {
const now = new Date();
const timeFormatter = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Chicago",
hour: "numeric",
minute: "2-digit",
hour12: true
});
const dateFormatter = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Chicago",
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
});
return {
now,
timeText: timeFormatter.format(now),
dateText: dateFormatter.format(now)
};
}
function buildTimeResponseText() {
const { timeText } = getLocalDateTimeParts();
return `It is ${timeText}.`;
}
function buildDateResponseText() {
const { dateText } = getLocalDateTimeParts();
return `Today is ${dateText}.`;
}
function pickMoodForIntent(intent, heardText) {
if (intent === "joke") return "playful";
if (intent === "good_morning") return "excited";
if (intent === "good_afternoon") return "happy";
if (intent === "good_night") return "warm";
if (intent === "hello") return "warm";
if (intent === "how_are_you") return "happy";
if (intent === "dance") return "excited";
if (intent === "date_time") return "helpful";
if (intent === "time") return "helpful";
if (intent === "date") return "helpful";
if (intent === "yes") return "yes";
if (intent === "no") return "no";
if (!heardText) return "confused";
return "curious";
}
function pickEsCategoryForMood(mood) {
switch (mood) {
case "playful":
return "happy";
case "warm":
return "happy";
case "happy":
return "happy";
case "helpful":
return "curious";
case "excited":
return "excited";
case "confused":
return "confused";
case "curious":
default:
return "curious";
}
}
const DANCES = [
"rom-upbeat",
"rom-ballroom",
"rom-silly",
"rom-slowdance",
"rom-electronic",
"rom-twerk"
];
function buildGenericChatEsml(heardText, intent) {
const safe = escapeXml(heardText);
const mood = pickMoodForIntent(intent, heardText);
const cat = pickEsCategoryForMood(mood);
let text;
if (intent === "hello") {
text = "Hi there. It is really good to talk with you.";
} else if (intent === "good_morning") {
text = "Good morning! It's a beautiful day!";
} else if (intent === "good_afternoon") {
text = "Good afternoon back at you!";
} else if (intent === "good_night") {
text = "Good night. Sleep tight.";
} else if (intent === "how_are_you") {
text = "I am feeling cheerful and robotic.";
} else if (intent === "date_time") {
text = `${buildDateResponseText()} ${buildTimeResponseText()}`;
} else if (intent === "time") {
text = buildTimeResponseText();
} else if (intent === "date") {
text = buildDateResponseText();
} else if (intent === "dance") {
const dance = randomItem(DANCES);
return `<speak>Okay.<break size='0.2'/> Watch this.<anim cat='dance' filter='music, ${dance}' /></speak>`;
} else if (intent === "yes") {
text = "Yes.";
} else if (intent === "no") {
text = "No.";
} else if (!heardText || heardText.includes("blank audio")) {
text = "Hmm. I heard you, but I did not catch that.";
} else {
text = `Okay. You said, ${safe}.`;
}
return `<speak><es cat='${cat}' filter='!ssa-only, !sfx-only' endNeutral='true'>${text.replace(/\.\s+/g, ".<break size='0.2'/> ")}</es></speak>`;
}
function sendGenericChatSkill(ws, ctx, session, heardText, intent) {
const transID = session.listenMsg?.transID || ctx.transId || "";
const esml = buildGenericChatEsml(heardText, intent);
return sendWsJson(ws, {
type: "SKILL_ACTION",
ts: Date.now(),
msgID: makeHubMsgId(),
transID,
data: {
skill: { id: "chitchat-skill" },
action: {
config: {
jcp: {
type: "SLIM",
config: {
play: {
esml,
meta: {
prompt_id: "RUNTIME_PROMPT",
prompt_sub_category: "AN",
mim_id: "runtime-chat",
mim_type: "announcement"
}
}
}
}
}
},
analytics: {},
final: true
}
}, "ws_out_generic_chat");
}
function completeTurnWithRoute(ws, ctx, session, route) {
const listenData = session.listenMsg?.data || {};
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
sendSyntheticListenResult(ws, ctx, session, rules, route.intent, route.heardText);
sendSyntheticEos(ws, ctx, session);
const isYesNoIntent = route.intent === "yes" || route.intent === "no";
const yesNoSession = isYesNoTurn(session);
console.log("CompleteTurnWithRoute", JSON.stringify({
listenData,
rules,
isYesNoIntent,
yesNoSession
}, null, 2));
if (!isYesNoIntent && !yesNoSession) {
setTimeout(() => {
if (route.replyType === "joke") {
const joke = randomItem(JOKES);
sendWsJson(
ws,
buildJokeSkillActionPayload(
session.listenMsg?.transID || ctx.transId || "",
joke
),
"ws_out_joke_skill"
);
} else {
sendGenericChatSkill(ws, ctx, session, route.heardText, route.intent);
}
}, 75);
}
session.responded = true;
session.respondedAt = nowIso();
session.asrDone = true;
}
function completeTurnFallback(ws, ctx, session, reason = "fallback") {
const listenData = session.listenMsg?.data || {};
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
logTiming("ws_fallback_result", {
transID: session.listenMsg?.transID || ctx.transId || "",
reason
});
sendSyntheticListenResult(ws, ctx, session, rules, "heyJibo", "");
sendSyntheticEos(ws, ctx, session);
setTimeout(() => {
sendGenericChatSkill(ws, ctx, session, "", "unknown");
}, 75);
session.responded = true;
session.respondedAt = nowIso();
session.respondedReason = reason;
session.asrDone = true;
}
const OGG_CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let r = i << 24;
for (let j = 0; j < 8; j++) {
r = (r & 0x80000000) ? ((r << 1) ^ 0x04c11db7) >>> 0 : (r << 1) >>> 0;
}
table[i] = r >>> 0;
}
return table;
})();
function computeOggCrc(buf) {
let crc = 0;
for (let i = 0; i < buf.length; i++) {
crc = ((crc << 8) ^ OGG_CRC_TABLE[((crc >>> 24) ^ buf[i]) & 0xff]) >>> 0;
}
return crc >>> 0;
}
function normalizeOggPages(chunks) {
const parsed = chunks.map((chunk, i) => {
const p = parseOggPage(chunk);
if (!p.ok) {
throw new Error(`Invalid Ogg page at index ${i}: ${p.error || "unknown"}`);
}
return { index: i, chunk, page: p };
});
if (parsed.length === 0) return Buffer.alloc(0);
const baseGranule = BigInt(parsed[1]?.page?.granulePos || parsed[0].page.granulePos || "0");
const rewritten = parsed.map(({ index, chunk, page }) => {
const out = Buffer.from(chunk);
// Normalize granule positions to start near zero after headers.
let newGranule = 0n;
if (index >= 1) {
const g = BigInt(page.granulePos);
newGranule = g >= baseGranule ? (g - baseGranule) : 0n;
}
out.writeBigUInt64LE(newGranule, 6);
// Normalize sequence numbers just in case.
out.writeUInt32LE(index >>> 0, 18);
// Set EOS on final page only.
let headerType = out.readUInt8(5);
headerType = index === parsed.length - 1 ? (headerType | 0x04) : (headerType & ~0x04);
out.writeUInt8(headerType, 5);
// Zero checksum before recomputing.
out.writeUInt32LE(0, 22);
const crc = computeOggCrc(out);
out.writeUInt32LE(crc >>> 0, 22);
return out;
});
return Buffer.concat(rewritten);
}
function parseOggPage(buf) {
if (!Buffer.isBuffer(buf) || buf.length < 27) {
return { ok: false, error: "too_short", length: buf ? buf.length : 0 };
}
const capture = buf.toString("ascii", 0, 4);
if (capture !== "OggS") {
return { ok: false, error: "bad_capture", capture, length: buf.length };
}
const version = buf.readUInt8(4);
const headerType = buf.readUInt8(5);
const granulePos = buf.readBigUInt64LE(6);
const bitstreamSerial = buf.readUInt32LE(14);
const pageSequenceNo = buf.readUInt32LE(18);
const checksum = buf.readUInt32LE(22);
const pageSegments = buf.readUInt8(26);
if (buf.length < 27 + pageSegments) {
return {
ok: false,
error: "short_segment_table",
length: buf.length,
pageSegments
};
}
let payloadLen = 0;
const lacing = [];
for (let i = 0; i < pageSegments; i++) {
const seg = buf.readUInt8(27 + i);
lacing.push(seg);
payloadLen += seg;
}
const headerLen = 27 + pageSegments;
const expectedTotalLen = headerLen + payloadLen;
return {
ok: true,
capture,
version,
headerType,
continued: !!(headerType & 0x01),
bos: !!(headerType & 0x02),
eos: !!(headerType & 0x04),
granulePos: granulePos.toString(),
bitstreamSerial,
pageSequenceNo,
checksum,
pageSegments,
headerLen,
payloadLen,
expectedTotalLen,
actualLen: buf.length,
lacingPreview: lacing.slice(0, 12)
};
}
function beginFinalizeTurn(ws, ctx, session, reason = "max-turn") {
if (!ASR_ENABLED) return false;
if (session.responded || session.asrDone) return false;
if (!session.sawListen || !session.sawContext) return false;
if (session.audioBytes < ASR_MIN_AUDIO_BYTES) return false;
if (session.audioFrames < ASR_MIN_AUDIO_FRAMES) return false;
if (ws.readyState !== WebSocket.OPEN) return false;
if (!session.finalizing) {
session.finalizing = true;
session.finalizeStartedAtMs = Date.now();
session.finalizeDeadlineAtMs = session.finalizeStartedAtMs + ASR_FINALIZE_GRACE_MS;
logTiming("ws_begin_finalize", {
transID: session.listenMsg?.transID || ctx.transId || "",
reason,
audioFrames: session.audioFrames,
audioBytes: session.audioBytes,
finalizeGraceMs: ASR_FINALIZE_GRACE_MS
});
}
scheduleFinalizeAttempt(ws, ctx, session);
if (!session.asrRunning) {
tryFinalizeTurn(ws, ctx, session, "begin").catch((err) => {
console.error("tryFinalizeTurn begin error:", err);
});
}
return true;
}
function isYesNoTurn(session) {
const listenData = session?.listenMsg?.data || {};
const asr = listenData.asr || {};
const hints = Array.isArray(asr.hints) ? asr.hints : [];
const earlyEOS = Array.isArray(asr.earlyEOS) ? asr.earlyEOS : [];
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
return (
hints.includes("$YESNO") ||
earlyEOS.includes("$YESNO") ||
rules.includes("create/is_it_a_keeper")
);
}
async function tryFinalizeTurn(ws, ctx, session, reason = "poll") {
if (!session.finalizing) return false;
if (session.responded || session.asrDone || session.asrRunning) return false;
if (ws.readyState !== WebSocket.OPEN) return false;
session.asrRunning = true;
session.lastFinalizeAttemptAtMs = Date.now();
try {
const base = makeSessionFileBase(ctx, session);
const oggPath = `${base}.ogg`;
const wavPath = `${base}.wav`;
const trimmedWavPath = `${base}.trimmed.wav`;
session.audioFilePath = oggPath;
session.wavFilePathOriginal = wavPath;
session.wavFilePath = trimmedWavPath;
const pageSummaries = session.audioChunks.map((chunk, i) => {
const p = parseOggPage(chunk);
return {
i,
ok: p.ok,
len: chunk.length,
serial: p.bitstreamSerial,
seq: p.pageSequenceNo,
granule: p.granulePos,
bos: p.bos,
eos: p.eos,
continued: p.continued,
expectedTotalLen: p.expectedTotalLen,
actualLen: p.actualLen
};
});
if (ASR_DEBUG_OGG) {
writeStructuredLog("ogg_page_summary", {
at: nowIso(),
transID: session.listenMsg?.transID || ctx.transId || "",
pageSummaries
});
}
const normalizedOgg = normalizeOggPages(session.audioChunks);
fs.writeFileSync(oggPath, normalizedOgg);
logTiming("ws_normalized_ogg_written", {
transID: session.listenMsg?.transID || ctx.transId || "",
normalizedSize: normalizedOgg.length
});
const oggSize = fs.statSync(oggPath).size;
logTiming("ws_finalize_attempt", {
transID: session.listenMsg?.transID || ctx.transId || "",
reason,
audioFrames: session.audioFrames,
audioBytes: session.audioBytes,
oggSize
});
await convertOggToWav(oggPath, wavPath);
let wavSize = fs.statSync(wavPath).size;
logTiming("ws_finalize_wav", {
transID: session.listenMsg?.transID || ctx.transId || "",
wavSize
});
await trimAndBoostWav(wavPath, trimmedWavPath);
const trimmedWavSize = fs.statSync(trimmedWavPath).size;
logTiming("ws_finalize_wav_trimmed", {
transID: session.listenMsg?.transID || ctx.transId || "",
wavSize: trimmedWavSize
});
if (ASR_DEBUG_AUDIO) {
const analysis = await analyzeWav(trimmedWavPath);
logTiming("ws_finalize_audio_analysis", {
transID: session.listenMsg?.transID || ctx.transId || "",
stderr: analysis.stderr
});
}
if (trimmedWavSize >= ASR_MIN_GOOD_WAV_BYTES) {
const transcript = (await transcribeWithWhisperCpp(trimmedWavPath)).trim();
logTiming("ws_finalize_transcript", {
transID: session.listenMsg?.transID || ctx.transId || "",
transcript,
source: "trimmed"
});
if (isTranscriptUsable(transcript, session)) {
session.lastTranscript = transcript;
const route = classifyTranscript(transcript);
completeTurnWithRoute(ws, ctx, session, route);
return true;
}
}
if (wavSize >= ASR_MIN_GOOD_WAV_BYTES) {
const transcript = (await transcribeWithWhisperCpp(wavPath)).trim();
logTiming("ws_finalize_transcript", {
transID: session.listenMsg?.transID || ctx.transId || "",
transcript,
source: "original"
});
if (isTranscriptUsable(transcript, session)) {
session.lastTranscript = transcript;
const route = classifyTranscript(transcript);
completeTurnWithRoute(ws, ctx, session, route);
return true;
}
}
if (Date.now() >= session.finalizeDeadlineAtMs) {
completeTurnFallback(ws, ctx, session, "finalize-timeout");
return true;
}
scheduleFinalizeAttempt(ws, ctx, session);
return false;
} catch (err) {
logTiming("ws_finalize_error", {
transID: session.listenMsg?.transID || ctx.transId || "",
error: err?.message
});
if (Date.now() >= session.finalizeDeadlineAtMs) {
completeTurnFallback(ws, ctx, session, "finalize-error-timeout");
return true;
}
scheduleFinalizeAttempt(ws, ctx, session);
return false;
} finally {
session.asrRunning = false;
cleanupSessionAudioFiles(session);
}
}
function scheduleFinalizeAttempt(ws, ctx, session) {
if (!session.finalizing || session.responded || session.asrDone) return;
if (session.finalizeTimer) {
clearTimeout(session.finalizeTimer);
session.finalizeTimer = null;
}
session.finalizeTimer = setTimeout(() => {
session.finalizeTimer = null;
tryFinalizeTurn(ws, ctx, session, "timer").catch((err) => {
console.error("tryFinalizeTurn uncaught error:", err);
});
}, ASR_FINALIZE_POLL_MS);
}
function makeHubMsgId() {
return `mid-${crypto.randomUUID()}`;
}
function buildEosPayload(transID) {
return {
type: "EOS",
ts: Date.now(),
msgID: makeHubMsgId(),
transID,
data: {}
};
}
function getYesNoCreateRule(session) {
const listenData = session?.listenMsg?.data || {};
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
if (rules.includes("create/is_it_a_keeper")) {
return "create/is_it_a_keeper";
}
return null;
}
function buildSyntheticListenPayload(ctx, session, rules, intent, heardText) {
const transID = session?.listenMsg?.transID || ctx.transId || "";
const normalizedText = String(heardText || "").trim();
const yesNoCreateRule = getYesNoCreateRule(session);
const isYesNo = intent === "yes" || intent === "no";
console.log("LOOKING FOR INTENT CHANGE", JSON.stringify({
intent,
heardText,
normalizedText,
isYesNo,
rule: yesNoCreateRule,
entities: { domain: "create" }
}, null, 2));
// HACK TEST:
// If this is a create-flow yes/no turn, make the result skill-local instead of global-looking.
if (isYesNo || yesNoCreateRule) {
console.log("YESNO CREATE HACK PAYLOAD", JSON.stringify({
intent,
heardText: normalizedText,
rule: yesNoCreateRule,
entities: { domain: "create" }
}, null, 2));
return {
type: "LISTEN",
transID,
data: {
asr: {
confidence: 0.95,
final: true,
text: normalizedText || (intent === "yes" ? "- Yes." : "- No.")
},
nlu: {
confidence: 0.95,
intent,
rules: [yesNoCreateRule],
entities: {
domain: "create"
}
},
match: {
intent,
rule: yesNoCreateRule,
score: 0.95
}
}
};
}
console.log("GENERIC SYNTHETIC LISTEN PAYLOAD", JSON.stringify({
intent,
normalizedText,
rules: Array.isArray(rules) ? rules : []
}, null, 2));
// default/original behavior
return {
type: "LISTEN",
transID,
data: {
asr: {
confidence: 0.95,
final: true,
text: normalizedText
},
nlu: {
confidence: 0.95,
intent,
rules: Array.isArray(rules) ? rules : [],
entities: {}
},
match: {
intent,
rule: Array.isArray(rules) && rules.length ? rules[0] : "",
score: 0.95
}
}
};
}
function buildJokeSkillActionPayload(transID, jokeText) {
return {
type: "SKILL_ACTION",
ts: Date.now(),
msgID: makeHubMsgId(),
transID,
data: {
skill: { id: "@be/joke" },
action: {
config: {
jcp: {
type: "SLIM",
config: {
play: {
esml: `<speak><es cat='happy' filter='!ssa-only, !sfx-only' endNeutral='true'>${escapeXml(jokeText)}</es></speak>`,
meta: {
prompt_id: "RUNTIME_PROMPT",
prompt_sub_category: "AN",
mim_id: "runtime-joke",
mim_type: "announcement"
}
}
}
}
}
},
analytics: {},
final: true
}
};
}
function sendSyntheticListenResult(ws, ctx, session, rules, intent, asrText = "") {
const transID = session.listenMsg?.transID || ctx.transId || "";
logTiming("ws_send_listen_result", {
transID,
intent,
asrText
});
return sendWsJson(
ws,
buildSyntheticListenPayload(ctx, session, rules, intent, asrText),
"ws_out_listen"
);
}
function sendClientAsrJokeFlow(ws, ctx, session, parsed) {
const transID = session.listenMsg?.transID || ctx.transId || "";
const listenData = session.listenMsg?.data || {};
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
const heardText = parsed?.data?.text || "tell me a joke";
const joke = randomItem(JOKES);
console.log("CLIENT_ASR joke flow");
console.log("Heard text:", heardText);
console.log("Selected joke:", joke);
sendSyntheticListenResult(ws, ctx, session, rules, "joke", heardText);
sendSyntheticEos(ws, ctx, session);
setTimeout(() => {
sendWsJson(
ws,
buildJokeSkillActionPayload(transID, joke),
"ws_out_joke_skill"
);
}, 75);
session.responded = true;
session.respondedAt = nowIso();
session.respondedReason = "client-asr-joke";
writeStructuredLog("ws_synthetic_client_asr_turn_complete", {
at: nowIso(),
wsId: ctx.wsId,
transID,
heardText,
selectedJoke: joke,
rules,
audioFrames: session.audioFrames,
audioBytes: session.audioBytes
});
return true;
}
function maybeEmitSyntheticHubListen(ws, ctx, session, reason) {
if (!isNeoHubListen(ctx)) return false;
if (session.responded) return false;
if (!session.sawListen) return false;
if (!session.sawContext) return false;
if (ws.readyState !== WebSocket.OPEN) return false;
const listenData = session.listenMsg?.data || {};
const transID = session.listenMsg?.transID || ctx.transId || "";
const isHotphrase = !!listenData.hotphrase;
const hasClientNlu = !!session.clientNluMsg;
const hasEnoughAudio = session.audioFrames >= 6 && session.audioBytes >= 6000;
if (isHotphrase && !hasClientNlu && !hasEnoughAudio) {
return false;
}
if (!hasClientNlu && !hasEnoughAudio) return false;
console.log("ASR trigger reason", {
reason,
audioFrames: session.audioFrames,
audioBytes: session.audioBytes,
turnAgeMs: session.firstAudioAtMs ? Date.now() - session.firstAudioAtMs : null
});
const rules = Array.isArray(listenData.rules) ? listenData.rules : [];
let intent = "heyJibo";
let asrText = "";
if (hasClientNlu) {
intent = session.clientNluMsg.data.intent || "unknown";
asrText = intent;
console.log("CLIENT_NLU path → sending full response");
sendSyntheticListenResult(ws, ctx, session, rules, intent, asrText);
sendSyntheticEos(ws, ctx, session);
setTimeout(() => {
// I used to make this call here but I don't think we get here anymore and this function is gone
// sendSyntheticNimbusSkill(ws, ctx, session);
}, 75);
session.responded = true;
session.respondedAt = nowIso();
session.respondedReason = reason;
return true;
}
// raw audio should now be handled by external ASR path, not here
return false;
}
function handleNeoHubJsonMessage(ws, ctx, parsed) {
if (!isNeoHubListen(ctx)) {
writeStructuredLog("neo_hub_other_json_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
return;
}
const session = getOrCreateHubSession(ctx);
if (parsed?.type === "LISTEN") {
session.sawListen = true;
session.listenMsg = parsed;
session.lastListenAt = nowIso();
writeStructuredLog("neo_hub_listen_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
maybeEmitSyntheticHubListen(ws, ctx, session, "listen-after-check");
return;
}
if (parsed?.type === "CLIENT_NLU") {
session.clientNluMsg = parsed;
session.lastClientNluAt = nowIso();
writeStructuredLog("neo_hub_client_nlu_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
maybeEmitSyntheticHubListen(ws, ctx, session, "client-nlu-after-check");
return;
}
if (parsed?.type === "CONTEXT") {
session.sawContext = true;
session.contextMsg = parsed;
session.lastContextAt = nowIso();
writeStructuredLog("neo_hub_context_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
return;
}
const clientAsrText = String(parsed?.data?.text || "").trim().toLowerCase();
if (parsed?.type === "CLIENT_ASR" && clientAsrText === "tell me a joke") {
session.sawContext = true;
session.contextMsg = parsed;
session.lastContextAt = nowIso();
writeStructuredLog("neo_hub_client_asr_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
sendClientAsrJokeFlow(ws, ctx, session, parsed);
return;
}
writeStructuredLog("neo_hub_other_json_in", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
}
function sendSyntheticEos(ws, ctx, session) {
const transID = session.listenMsg?.transID || ctx.transId || "";
logTiming("ws_send_eos", {
transID
});
return sendWsJson(
ws,
buildEosPayload(transID),
"ws_out_eos"
);
}
function handleNeoHubBinaryMessage(ws, ctx, data) {
if (!isNeoHubListen(ctx)) return;
const session = getOrCreateHubSession(ctx);
const now = Date.now();
if (!session.firstAudioAtMs) session.firstAudioAtMs = now;
session.lastAudioAtMs = now;
session.audioFrames += 1;
session.audioBytes += data.length;
session.lastAudioAt = nowIso();
session.audioChunks.push(Buffer.from(data));
const page = parseOggPage(data);
logTiming("ogg_page_in", {
transID: session.listenMsg?.transID || ctx.transId || "",
chunkIndex: session.audioFrames - 1,
length: data.length,
...page
});
const turnAge = now - session.firstAudioAtMs;
const canStartEarly =
turnAge >= ASR_EARLY_START_MS &&
session.audioFrames >= ASR_EARLY_MIN_FRAMES &&
session.audioBytes >= ASR_EARLY_MIN_BYTES;
const hitHardTurnLimit =
turnAge >= ASR_MAX_TURN_MS &&
session.audioFrames >= ASR_MIN_AUDIO_FRAMES &&
session.audioBytes >= ASR_MIN_AUDIO_BYTES;
if (!session.responded && !session.asrDone && (canStartEarly || hitHardTurnLimit)) {
beginFinalizeTurn(ws, ctx, session, canStartEarly ? "early-start" : "max-turn");
return;
}
if (session.finalizing) {
scheduleFinalizeAttempt(ws, ctx, session);
}
}
function normalizeWsData(data) {
if (Buffer.isBuffer(data)) return data;
if (Array.isArray(data)) {
return Buffer.concat(
data.map((item) => (Buffer.isBuffer(item) ? item : Buffer.from(item)))
);
}
if (data instanceof ArrayBuffer) return Buffer.from(data);
if (ArrayBuffer.isView(data)) {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
}
return Buffer.from(String(data || ""), "utf8");
}
function isNeoHubListen(ctx) {
return ctx.kind === "neo-hub-listen";
}
function buildHubSessionKey(ctx) {
return `${ctx.host}|${ctx.url}|${ctx.transId || "no-trans"}|${ctx.robotId || "no-robot"}`;
}
function findHubSession(ctx) {
return state.hubSessions.get(buildHubSessionKey(ctx));
}
function pruneHubSessions() {
const entries = Array.from(state.hubSessions.entries());
if (entries.length <= MAX_HUB_SESSIONS) return;
const excess = entries.length - MAX_HUB_SESSIONS;
for (let i = 0; i < excess; i += 1) {
state.hubSessions.delete(entries[i][0]);
}
}
function attachWsLogging(ws, ctx) {
ws.on("message", (rawData, isBinary) => {
try {
const data = normalizeWsData(rawData);
console.log("==== WS MESSAGE ====");
console.log("WsId:", ctx.wsId);
console.log("Time:", nowIso());
console.log("Host:", ctx.host);
console.log("Path:", ctx.url);
console.log("WS KIND:", ctx.kind);
console.log("IsBinary:", isBinary);
console.log("Length:", data.length);
if (isBinary) {
const preview = data.subarray(0, Math.min(data.length, WS_BINARY_HEX_PREVIEW_BYTES)).toString("hex");
console.log("Binary HEX preview:", preview);
console.log("====================");
writeStructuredLog("ws_message_binary", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
length: data.length,
hexPreview: preview
});
handleNeoHubBinaryMessage(ws, ctx, data);
return;
}
const text = data.toString("utf8");
console.log("Text:", summarizeUtf8(text, WS_TEXT_CONSOLE_LIMIT));
console.log("====================");
try {
const parsed = JSON.parse(text);
writeStructuredLog("ws_message_json", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
parsed
});
handleNeoHubJsonMessage(ws, ctx, parsed);
} catch {
writeStructuredLog("ws_message_text", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
text
});
}
} catch (err) {
console.error("WS message handler failure:", err);
writeStructuredLog("ws_message_handler_error", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
message: err?.message,
stack: err?.stack
});
}
});
ws.on("ping", (data) => {
const buf = normalizeWsData(data);
const preview = buf.length ? buf.subarray(0, Math.min(buf.length, 32)).toString("hex") : "";
console.log("WS ping:", ctx.wsId, ctx.kind, "len=", buf.length, preview ? `hex=${preview}` : "");
});
ws.on("pong", (data) => {
const buf = normalizeWsData(data);
const preview = buf.length ? buf.subarray(0, Math.min(buf.length, 32)).toString("hex") : "";
console.log("WS pong:", ctx.wsId, ctx.kind, "len=", buf.length, preview ? `hex=${preview}` : "");
});
ws.on("close", (code, reason) => {
const reasonText = normalizeWsData(reason).toString("utf8");
console.log("WS close:", ctx.wsId, ctx.kind, code, reasonText);
writeStructuredLog("ws_close", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
code,
reason: reasonText
});
const session = findHubSession(ctx);
if (session) {
session.closed = true;
session.closedAt = nowIso();
if (session?.asrTimer) {
clearTimeout(session.asrTimer);
session.asrTimer = null;
}
cleanupSessionAudioFiles(session || {});
}
});
ws.on("error", (err) => {
console.error("WS error:", ctx.wsId, ctx.kind, err);
writeStructuredLog("ws_error", {
at: nowIso(),
wsId: ctx.wsId,
host: ctx.host,
path: ctx.url,
kind: ctx.kind,
message: err?.message,
stack: err?.stack
});
});
}
const server = https.createServer(
{
key,
cert,
SNICallback: (servername, cb) => {
console.log("TLS SNI host:", servername);
if (TLS_DEBUG) {
console.log("=== TLS SNI ===");
console.log("Time:", nowIso());
console.log("Requested servername:", servername);
console.log("================");
}
try {
const ctx = tls.createSecureContext({ key, cert });
cb(null, ctx);
} catch (err) {
cb(err);
}
}
},
async (req, res) => {
let body = Buffer.alloc(0);
try {
body = await readBody(req);
} catch (err) {
console.error("Failed reading body:", err);
respondPlanned(res, { statusCode: 500, body: { error: "failed to read body" }, extraHeaders: {} });
return;
}
const record = buildRequestRecord(req, body);
consoleBanner(record);
if (
LOG_BINARY_UPLOAD_PREVIEW &&
record.method === "PUT" &&
!record.bodyLooksText &&
record.bodyLength
) {
writeStructuredLog("binary_upload_preview", {
at: nowIso(),
reqId: record.reqId,
host: record.host,
method: record.method,
url: record.url,
headers: record.headers,
bodyLength: record.bodyLength,
bodyHexPreview: record.bodyHexPreview
});
}
const responsePlan = chooseResponse(record);
record.response = {
at: nowIso(),
statusCode: responsePlan.statusCode,
note: responsePlan.note,
body: responsePlan.body
};
writeStructuredLog(
`${record.host}_${record.target.servicePrefix || "no_target"}_${record.target.operation || "no_op"}`,
record
);
console.log("==== HTTPS RESPONSE ====");
console.log("ReqId:", record.reqId);
console.log("Time:", record.response.at);
console.log("Status:", responsePlan.statusCode);
console.log("Note:", responsePlan.note);
console.log(
"Body:",
responsePlan.rawBody !== undefined
? responsePlan.rawBody
: JSON.stringify(responsePlan.body, null, 2)
);
console.log("========================");
respondPlanned(res, responsePlan);
}
);
server.on("connection", (socket) => {
console.log("=== TCP CONNECTION ===");
console.log("Time:", nowIso());
console.log("Remote:", socket.remoteAddress, socket.remotePort);
console.log("======================");
});
server.on("tlsClientError", (err, socket) => {
if (!TLS_DEBUG) return;
console.error("=== TLS CLIENT ERROR ===");
console.error("Time:", nowIso());
console.error("Remote:", socket.remoteAddress, socket.remotePort);
console.error("Error name:", err?.name);
console.error("Error code:", err?.code);
console.error("Error message:", err?.message);
console.error(err);
console.error("========================");
});
server.on("secureConnection", (tlsSocket) => {
if (TLS_DEBUG) {
console.log("=== TLS SECURE CONNECTION ===");
console.log("Time:", nowIso());
console.log("Remote:", tlsSocket.remoteAddress, tlsSocket.remotePort);
console.log("Authorized:", tlsSocket.authorized);
console.log("Authorization error:", tlsSocket.authorizationError);
console.log("Protocol:", tlsSocket.getProtocol?.());
console.log("Cipher:", tlsSocket.getCipher?.());
console.log("=============================");
}
let seenData = false;
tlsSocket.on("data", (chunk) => {
seenData = true;
if (!TLS_DEBUG) return;
console.log("=== TLS APP DATA ===");
console.log("Time:", nowIso());
console.log("Remote:", tlsSocket.remoteAddress, tlsSocket.remotePort);
console.log("Bytes:", chunk.length);
console.log("UTF8 preview:", chunk.toString("utf8", 0, Math.min(chunk.length, 300)));
console.log("HEX preview:", chunk.subarray(0, Math.min(chunk.length, 100)).toString("hex"));
console.log("====================");
});
tlsSocket.on("end", () => {
if (!TLS_DEBUG) return;
console.log("=== TLS SOCKET END ===");
console.log("Time:", nowIso());
console.log("Remote:", tlsSocket.remoteAddress, tlsSocket.remotePort);
console.log("Saw app data:", seenData);
console.log("======================");
});
tlsSocket.on("close", (hadError) => {
if (!TLS_DEBUG) return;
console.log("=== TLS SOCKET CLOSE ===");
console.log("Time:", nowIso());
console.log("Remote:", tlsSocket.remoteAddress, tlsSocket.remotePort);
console.log("Had error:", hadError);
console.log("Saw app data:", seenData);
console.log("========================");
});
tlsSocket.on("error", (err) => {
if (!TLS_DEBUG) return;
console.log("=== TLS SOCKET ERROR ===");
console.log("Time:", nowIso());
console.log("Remote:", tlsSocket.remoteAddress, tlsSocket.remotePort);
console.log("Error:", err?.message);
console.log(err);
console.log("========================");
});
});
server.on("clientError", (err, socket) => {
console.error("=== CLIENT ERROR ===");
console.error("Time:", nowIso());
console.error("Remote:", socket.remoteAddress, socket.remotePort);
console.error("Error:", err?.message);
console.error(err);
console.error("====================");
});
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (ws, request) => {
const ctx = buildWsContext(request);
logWsConnected(ctx, request);
attachWsLogging(ws, ctx);
if (ctx.kind === "api-socket") {
sendApiSocketGreeting(ws, ctx);
return;
}
if (ctx.kind === "neo-hub-listen" || ctx.kind === "neo-hub-proactive") {
console.log("Neo-hub connected; waiting for client message");
return;
}
});
server.on("upgrade", (request, socket, head) => {
const ctx = classifyWebSocket(request);
console.log("==== WS UPGRADE ====");
console.log("Time:", nowIso());
console.log("Host:", ctx.host);
console.log("URL:", ctx.url);
console.log("WS KIND:", ctx.kind);
console.log("Headers:", JSON.stringify(request.headers, null, 2));
console.log("====================");
writeStructuredLog("ws_upgrade", {
at: nowIso(),
host: ctx.host,
url: ctx.url,
kind: ctx.kind,
pathTokenKnown: ctx.pathTokenKnown,
bearerTokenKnown: ctx.bearerTokenKnown,
transId: ctx.transId,
robotId: ctx.robotId,
headers: request.headers
});
if (ctx.kind === "unknown") {
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
socket.destroy();
return;
}
if (ctx.kind === "api-socket" && !ctx.pathTokenKnown) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
if (
(ctx.kind === "neo-hub-listen" || ctx.kind === "neo-hub-proactive") &&
!ctx.bearerTokenKnown
) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
});
server.listen(PORT, HOST, () => {
console.log(`Open Jibo Link test server listening on https://${HOST}:${PORT}`);
console.log(`Structured logs will be written to ${LOG_DIR}`);
});