neotoma 0.21.0 → 0.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +18 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +152 -36
- package/dist/actions.js.map +1 -1
- package/dist/cli/discovery.d.ts.map +1 -1
- package/dist/cli/discovery.js +34 -1
- package/dist/cli/discovery.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +66 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/onboarding_transcript_import.d.ts +18 -0
- package/dist/cli/onboarding_transcript_import.d.ts.map +1 -1
- package/dist/cli/onboarding_transcript_import.js +98 -0
- package/dist/cli/onboarding_transcript_import.js.map +1 -1
- package/dist/cli/transcript_parser.d.ts +29 -1
- package/dist/cli/transcript_parser.d.ts.map +1 -1
- package/dist/cli/transcript_parser.js +117 -3
- package/dist/cli/transcript_parser.js.map +1 -1
- package/dist/docs/api/rest_api.md +24 -0
- package/dist/docs/developer/cli/transcript_ingestion.md +90 -0
- package/dist/docs/developer/cli_reference.md +38 -11
- package/dist/docs/security/advisories/2026-08-07-ed25519-bearer-forged-key-auth-bypass.md +67 -0
- package/dist/docs/security/advisories/2026-08-07-sort-by-order-by-sql-injection.md +64 -0
- package/dist/docs/security/advisories/README.md +2 -0
- package/dist/docs/testing/automated_test_catalog.md +22 -12
- package/dist/repositories/sqlite/local_db_adapter.d.ts +1 -0
- package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
- package/dist/repositories/sqlite/local_db_adapter.js +33 -3
- package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -2
- package/dist/server.js.map +1 -1
- package/dist/services/entity_queries.d.ts +4 -0
- package/dist/services/entity_queries.d.ts.map +1 -1
- package/dist/services/entity_queries.js +30 -0
- package/dist/services/entity_queries.js.map +1 -1
- package/dist/services/schema_registry.d.ts +7 -0
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +13 -0
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/sessions/seed_schema.d.ts +25 -0
- package/dist/services/sessions/seed_schema.d.ts.map +1 -0
- package/dist/services/sessions/seed_schema.js +139 -0
- package/dist/services/sessions/seed_schema.js.map +1 -0
- package/dist/shared/action_schemas.d.ts +40 -40
- package/dist/shared/contract_mappings.d.ts.map +1 -1
- package/dist/shared/contract_mappings.js +20 -1
- package/dist/shared/contract_mappings.js.map +1 -1
- package/dist/shared/openapi_types.d.ts +154 -3
- package/dist/shared/openapi_types.d.ts.map +1 -1
- package/openapi.yaml +111 -2
- package/package.json +1 -1
package/dist/actions.d.ts
CHANGED
|
@@ -187,6 +187,24 @@ export declare function routeAcceptsAnonymousIssueSubmit(req: Pick<express.Reque
|
|
|
187
187
|
* be resolved — prevents silent mis-attribution.
|
|
188
188
|
*/
|
|
189
189
|
export declare function resolveGuestUserId(req: express.Request, principal: RoutePrincipal): Promise<string | null>;
|
|
190
|
+
/**
|
|
191
|
+
* Helper to extract authenticated user_id from request.
|
|
192
|
+
* Supports: middleware-set user (Bearer / OAuth / dev-local / AAuth admission),
|
|
193
|
+
* session token, Ed25519 bearer.
|
|
194
|
+
*
|
|
195
|
+
* Stronger AAuth Admission plan: admitted AAuth callers carry
|
|
196
|
+
* `req.authenticatedUserId` resolved from the matching `agent_grant`'s
|
|
197
|
+
* owner. This helper applies the same `user_id` mismatch rules to them
|
|
198
|
+
* as to OAuth/Bearer — payload-driven user pivots are rejected. The
|
|
199
|
+
* local-dev override remains available only for the local dev user;
|
|
200
|
+
* AAuth admission can never resolve to that id, so admitted agents are
|
|
201
|
+
* structurally locked to their grant owner.
|
|
202
|
+
* @param req - Express request object
|
|
203
|
+
* @param providedUserId - Optional user_id from request body/query
|
|
204
|
+
* @returns Authenticated user_id
|
|
205
|
+
* @throws Error if not authenticated or user_id mismatch
|
|
206
|
+
*/
|
|
207
|
+
export declare function getAuthenticatedUserId(req: express.Request, providedUserId?: string): Promise<string>;
|
|
190
208
|
type StoreRelationshipRef = {
|
|
191
209
|
relationship_type: string;
|
|
192
210
|
source_index?: number;
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAuC9B,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAarC,OAAO,EAGL,KAAK,aAAa,EAGnB,MAAM,4BAA4B,CAAC;AA4BpC,OAAO,EAQL,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AAkDpC,OAAO,EAGL,oBAAoB,EACrB,MAAM,8BAA8B,CAAC;AAWtC,OAAO,EAiCL,KAAK,wBAAwB,EAE9B,MAAM,4BAA4B,CAAC;AA8CpC,6FAA6F;AAC7F,wBAAgB,qBAAqB,IAAI,sBAAsB,GAAG,IAAI,CAErE;AAED,eAAO,MAAM,GAAG,6CAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/actions.js
CHANGED
|
@@ -628,7 +628,14 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
|
|
628
628
|
token_endpoint_auth_methods_supported: ["none"],
|
|
629
629
|
});
|
|
630
630
|
});
|
|
631
|
-
|
|
631
|
+
// Both the bare path and the resource-suffixed form are served (#2049).
|
|
632
|
+
// RFC 9728 §3.1 locates a resource's metadata by appending the resource's own
|
|
633
|
+
// path, so a client whose resource is `/mcp` probes
|
|
634
|
+
// `/.well-known/oauth-protected-resource/mcp`. Express does not match that
|
|
635
|
+
// against the bare route, so it previously fell through to the catch-all auth
|
|
636
|
+
// guard and 401'd — the same bootstrap failure as the deadlock described below,
|
|
637
|
+
// reached by a different path. Registering both makes either probe order work.
|
|
638
|
+
app.get(["/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp"], async (req, res) => {
|
|
632
639
|
const forwardedProto = req.header("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
633
640
|
const forwardedHost = req.header("x-forwarded-host")?.split(",")[0]?.trim();
|
|
634
641
|
const host = forwardedHost || req.header("host");
|
|
@@ -640,8 +647,18 @@ app.get("/.well-known/oauth-protected-resource", async (req, res) => {
|
|
|
640
647
|
has_authorization: Boolean((req.headers["authorization"] || req.headers["Authorization"])),
|
|
641
648
|
has_connection_id: Boolean(req.headers["x-connection-id"] || req.headers["X-Connection-Id"]),
|
|
642
649
|
});
|
|
643
|
-
// oauth-repro pattern: return 401 on protected resource endpoint when unauthenticated
|
|
644
650
|
// Validate X-Connection-Id here so Cursor gets consistent invalid_token signal (helps trigger Connect prompt)
|
|
651
|
+
//
|
|
652
|
+
// This document is NOT gated on authentication (#2049). RFC 9728 §3 makes
|
|
653
|
+
// protected-resource metadata the unauthenticated bootstrap entry point: its
|
|
654
|
+
// entire job is to tell a caller with no credentials where to obtain them. An
|
|
655
|
+
// earlier revision returned 401 here when neither Authorization nor
|
|
656
|
+
// X-Connection-Id was present, with a `WWW-Authenticate` naming THIS URL as
|
|
657
|
+
// the place to look — a self-referential deadlock. A client 401'd at /mcp
|
|
658
|
+
// follows the header here, is 401'd again pointing back here, and the login
|
|
659
|
+
// flow never starts, so no credential can resolve it. The sibling
|
|
660
|
+
// /.well-known/oauth-authorization-server was always public (RFC 8414 requires
|
|
661
|
+
// the same); that asymmetry was the bug, not the design.
|
|
645
662
|
const authHeader = (req.headers["authorization"] || req.headers["Authorization"]);
|
|
646
663
|
const connectionIdHeader = req.headers["x-connection-id"] || req.headers["X-Connection-Id"];
|
|
647
664
|
// If X-Connection-Id is sent, validate it. Invalid/expired connection → 401 with error=invalid_token
|
|
@@ -660,17 +677,35 @@ app.get("/.well-known/oauth-protected-resource", async (req, res) => {
|
|
|
660
677
|
});
|
|
661
678
|
}
|
|
662
679
|
}
|
|
663
|
-
if (!authHeader?.startsWith("Bearer ") && !connectionIdHeader) {
|
|
664
|
-
res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`);
|
|
665
|
-
return res.status(401).json({
|
|
666
|
-
error: "Unauthorized: Authentication required",
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
680
|
res.setHeader("Content-Type", "application/json");
|
|
670
681
|
res.json({
|
|
682
|
+
// RFC 9728 §3: `resource` is the identifier the client binds its token
|
|
683
|
+
// request to. Without it a client that validates the metadata document
|
|
684
|
+
// cannot confirm the document describes the resource it is trying to
|
|
685
|
+
// reach, and has to guess.
|
|
686
|
+
resource: `${base}/mcp`,
|
|
671
687
|
authorization_servers: [base],
|
|
672
688
|
});
|
|
673
689
|
});
|
|
690
|
+
// OIDC discovery is not offered — return an explicit 404 rather than letting this
|
|
691
|
+
// path fall through to the catch-all auth guard, which answers 401 (#2049).
|
|
692
|
+
//
|
|
693
|
+
// A 401 on a route that does not exist tells an OIDC-first client "this endpoint
|
|
694
|
+
// is real, you are just unauthenticated", sending it into the same dead end the
|
|
695
|
+
// protected-resource deadlock created: it retries against a document that will
|
|
696
|
+
// never serve it. A 404 says "not offered here", which correctly makes the client
|
|
697
|
+
// fall back to RFC 8414 /.well-known/oauth-authorization-server.
|
|
698
|
+
//
|
|
699
|
+
// Deliberately NOT a synthetic 200 alias of the OAuth authorization-server
|
|
700
|
+
// metadata: that would advertise a second, parallel discovery contract Neotoma
|
|
701
|
+
// does not actually implement. Revisit only if a real OIDC stack is scoped.
|
|
702
|
+
app.get("/.well-known/openid-configuration", (_req, res) => {
|
|
703
|
+
res.status(404).json({
|
|
704
|
+
error: "not_found",
|
|
705
|
+
error_description: "This server does not offer OpenID Connect discovery. Use " +
|
|
706
|
+
"/.well-known/oauth-authorization-server (RFC 8414) instead.",
|
|
707
|
+
});
|
|
708
|
+
});
|
|
674
709
|
// AAuth resource server metadata. Exposed publicly so AAuth client libraries
|
|
675
710
|
// can auto-configure issuer / supported algs / signature window without an
|
|
676
711
|
// out-of-band conversation. Neotoma is verifier-only: it never hosts agent
|
|
@@ -3358,28 +3393,42 @@ app.use(async (req, res, next) => {
|
|
|
3358
3393
|
});
|
|
3359
3394
|
}
|
|
3360
3395
|
const bearerToken = headerAuth.slice("Bearer ".length).trim();
|
|
3361
|
-
//
|
|
3396
|
+
// Ed25519 bearer path.
|
|
3397
|
+
//
|
|
3398
|
+
// SECURITY (advisory 2026-08-07-ed25519-bearer-forged-key-auth-bypass):
|
|
3399
|
+
// The bearer token IS a public key (base64url of 32 raw bytes). Possession
|
|
3400
|
+
// of a public key must NEVER authenticate — anyone can present any public
|
|
3401
|
+
// key. Two invariants close the prior forged-key bypass:
|
|
3402
|
+
//
|
|
3403
|
+
// 1. A valid request Signature is MANDATORY. It proves possession of the
|
|
3404
|
+
// corresponding private key. A request without a signature is rejected,
|
|
3405
|
+
// not silently accepted (the old `if (signature && req.body)` made the
|
|
3406
|
+
// check optional, so an unsigned forged key sailed through).
|
|
3407
|
+
// 2. The key must map to a PRE-PROVISIONED userId. `ensurePublicKeyRegistered`
|
|
3408
|
+
// auto-registers any syntactically-valid 32-byte key, so its `true` return
|
|
3409
|
+
// means "well-formed", not "known". Only a key that was registered WITH a
|
|
3410
|
+
// userId (getUserIdFromBearerToken) is a real principal; a bare
|
|
3411
|
+
// auto-registered key resolves to `undefined` and must fall through to the
|
|
3412
|
+
// reject path — never to a caller-supplied `user_id` in getAuthenticatedUserId.
|
|
3362
3413
|
const registered = ensurePublicKeyRegistered(bearerToken);
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
return sendError(res, 403, "AUTH_INVALID", "Invalid request signature");
|
|
3372
|
-
}
|
|
3414
|
+
const registeredUserId = registered ? getUserIdFromBearerToken(bearerToken) : undefined;
|
|
3415
|
+
const { signature: ed25519Signature } = parseAuthHeader(headerAuth);
|
|
3416
|
+
if (registered && isBearerTokenValid(bearerToken) && registeredUserId && ed25519Signature) {
|
|
3417
|
+
// Signature is required and must verify over the exact request body.
|
|
3418
|
+
const bodyString = typeof req.body === "string" ? req.body : JSON.stringify(req.body ?? "");
|
|
3419
|
+
if (!verifyRequest(bodyString, ed25519Signature, bearerToken)) {
|
|
3420
|
+
logWarn("AuthInvalidSignature", req);
|
|
3421
|
+
return sendError(res, 403, "AUTH_INVALID", "Invalid request signature");
|
|
3373
3422
|
}
|
|
3374
3423
|
// Attach public key to request for encryption service
|
|
3375
3424
|
req.publicKey = getPublicKey(bearerToken);
|
|
3376
3425
|
req.bearerToken = bearerToken;
|
|
3377
|
-
//
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
logger.info(`[Auth] ${req.method} ${req.path} auth_method=ed25519_bearer user_id=${
|
|
3426
|
+
// Bind the request to the pre-provisioned principal. The caller cannot
|
|
3427
|
+
// override this via a `user_id` param — getAuthenticatedUserId keys off the
|
|
3428
|
+
// stamped principal, and the Ed25519 fall-through that trusted a
|
|
3429
|
+
// caller-supplied user_id has been removed.
|
|
3430
|
+
stampUserPrincipal(req, registeredUserId);
|
|
3431
|
+
logger.info(`[Auth] ${req.method} ${req.path} auth_method=ed25519_bearer user_id=${registeredUserId}`);
|
|
3383
3432
|
}
|
|
3384
3433
|
else {
|
|
3385
3434
|
// Try to validate as session token
|
|
@@ -3484,7 +3533,10 @@ app.get("/me", async (req, res) => {
|
|
|
3484
3533
|
* @returns Authenticated user_id
|
|
3485
3534
|
* @throws Error if not authenticated or user_id mismatch
|
|
3486
3535
|
*/
|
|
3487
|
-
|
|
3536
|
+
// Exported for the security regression test that asserts the fail-closed
|
|
3537
|
+
// behaviour for unresolved Bearer principals
|
|
3538
|
+
// (advisory 2026-08-07-ed25519-bearer-forged-key-auth-bypass).
|
|
3539
|
+
export async function getAuthenticatedUserId(req, providedUserId) {
|
|
3488
3540
|
const principal = requestPrincipal(req);
|
|
3489
3541
|
if (principal?.kind === "guest") {
|
|
3490
3542
|
throw new Error("Not authenticated - guest principal cannot resolve a user_id");
|
|
@@ -3510,16 +3562,17 @@ async function getAuthenticatedUserId(req, providedUserId) {
|
|
|
3510
3562
|
}
|
|
3511
3563
|
return authenticatedUserId;
|
|
3512
3564
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
//
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
//
|
|
3522
|
-
return
|
|
3565
|
+
// No principal was resolved by the auth middleware. Fail closed.
|
|
3566
|
+
//
|
|
3567
|
+
// SECURITY (advisory 2026-08-07-ed25519-bearer-forged-key-auth-bypass):
|
|
3568
|
+
// Previously this tail returned the caller-supplied `user_id` for any request
|
|
3569
|
+
// carrying a Bearer header, on the assumption that "token validation happens
|
|
3570
|
+
// in middleware". A forged public key was accepted by that middleware without
|
|
3571
|
+
// proving possession of the private key, so this line handed an attacker the
|
|
3572
|
+
// scope of their choice (e.g. the shared nil-UUID owner). A request that
|
|
3573
|
+
// reaches here has NOT been authenticated to any principal — there is no
|
|
3574
|
+
// trustworthy `user_id` to return, regardless of what the caller provided.
|
|
3575
|
+
throw new Error("Not authenticated - no resolved principal for request");
|
|
3523
3576
|
}
|
|
3524
3577
|
/**
|
|
3525
3578
|
* Helper to handle errors in API endpoints, including authentication errors
|
|
@@ -3583,6 +3636,24 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
|
|
|
3583
3636
|
.status(error.status)
|
|
3584
3637
|
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelopeDetails()));
|
|
3585
3638
|
}
|
|
3639
|
+
// SECURITY (advisory 2026-08-07-sort-by-order-by-sql-injection): a rejected
|
|
3640
|
+
// snapshot field name (sort_by / snapshot_filters) or an unsafe column
|
|
3641
|
+
// reference caught by the sqlite adapter is a client input error, not a server
|
|
3642
|
+
// fault — surface it as a 400 so it is not masked as a 500 DB_QUERY_FAILED, and
|
|
3643
|
+
// so the negative test asserts a stable status. Matched by name to avoid a
|
|
3644
|
+
// static import cycle with the dynamically-imported query module.
|
|
3645
|
+
if (error instanceof Error &&
|
|
3646
|
+
(error.name === "InvalidSnapshotFieldError" ||
|
|
3647
|
+
error.message.startsWith("Unsafe column reference rejected:"))) {
|
|
3648
|
+
// Log the full message (with the rejected value) server-side for debugging,
|
|
3649
|
+
// but do NOT echo the caller-supplied field string back in the response —
|
|
3650
|
+
// reflecting rejected input, even sanitized, is needless. Return a generic
|
|
3651
|
+
// detail that names the constraint. (Vanellus review, PR #2129, non-blocking.)
|
|
3652
|
+
logWarn(logContext || "InvalidQueryField", req, { detail: error.message });
|
|
3653
|
+
return res.status(400).json(buildErrorEnvelope("INVALID_QUERY_FIELD", "Invalid query field name.", {
|
|
3654
|
+
detail: "Query field names (sort_by / snapshot_filters keys) must be bare identifiers matching ^[A-Za-z_][A-Za-z0-9_]*$.",
|
|
3655
|
+
}));
|
|
3656
|
+
}
|
|
3586
3657
|
logError(logContext || "APIError", req, error);
|
|
3587
3658
|
// SECURITY: do not echo raw Error.message to clients in production; SQLite /
|
|
3588
3659
|
// filesystem errors frequently leak paths and schema. In development we keep
|
|
@@ -9609,6 +9680,15 @@ export async function startHTTPServer() {
|
|
|
9609
9680
|
catch (err) {
|
|
9610
9681
|
logger.warn(`[Plans] failed to seed plan schema: ${err.message}`);
|
|
9611
9682
|
}
|
|
9683
|
+
// Seed `agent_session` + `session_transcript` schemas (transcript ingest).
|
|
9684
|
+
try {
|
|
9685
|
+
const { seedSessionSchemas } = await import("./services/sessions/seed_schema.js");
|
|
9686
|
+
await seedSessionSchemas();
|
|
9687
|
+
logger.info("[Sessions] agent_session + session_transcript schemas seeded");
|
|
9688
|
+
}
|
|
9689
|
+
catch (err) {
|
|
9690
|
+
logger.warn(`[Sessions] failed to seed session schemas: ${err.message}`);
|
|
9691
|
+
}
|
|
9612
9692
|
// Seed `skill` schema (harness-mirrored skills, slash-command palette).
|
|
9613
9693
|
try {
|
|
9614
9694
|
const { seedSkillSchema } = await import("./services/skills/seed_schema.js");
|
|
@@ -9775,6 +9855,42 @@ export async function startHTTPServer() {
|
|
|
9775
9855
|
// Only auto-start if not disabled AND if this is the main module
|
|
9776
9856
|
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
9777
9857
|
if (process.env.NEOTOMA_ACTIONS_DISABLE_AUTOSTART !== "1" && isMainModule) {
|
|
9858
|
+
// Exit diagnostics. A long-running server should never reach `beforeExit`:
|
|
9859
|
+
// that event only fires when the event loop has drained, i.e. nothing is
|
|
9860
|
+
// left holding the process open. In production this happened silently and
|
|
9861
|
+
// repeatedly — the process exited with code 0 every ~12 minutes with no
|
|
9862
|
+
// error and no signal, and because the exit was CLEAN the platform read it
|
|
9863
|
+
// as success and declined to restart, turning it into an outage
|
|
9864
|
+
// (neotoma#2094).
|
|
9865
|
+
//
|
|
9866
|
+
// Logging the active resources at that moment names what the loop was (and
|
|
9867
|
+
// was no longer) waiting on, which is the one piece of evidence needed to
|
|
9868
|
+
// find the handle that stops holding the server up. `exit` is logged too so
|
|
9869
|
+
// the code and reason are attributable even when the cause is external.
|
|
9870
|
+
process.on("beforeExit", (code) => {
|
|
9871
|
+
let resources = [];
|
|
9872
|
+
try {
|
|
9873
|
+
const getResources = process
|
|
9874
|
+
.getActiveResourcesInfo;
|
|
9875
|
+
if (typeof getResources === "function")
|
|
9876
|
+
resources = getResources.call(process);
|
|
9877
|
+
}
|
|
9878
|
+
catch {
|
|
9879
|
+
/* diagnostics must never themselves throw */
|
|
9880
|
+
}
|
|
9881
|
+
console.error(`[neotoma] beforeExit code=${code} — event loop drained; a long-running ` +
|
|
9882
|
+
`server should not reach here. activeResources=${JSON.stringify(resources)} ` +
|
|
9883
|
+
`uptime=${process.uptime().toFixed(1)}s`);
|
|
9884
|
+
});
|
|
9885
|
+
process.on("exit", (code) => {
|
|
9886
|
+
console.error(`[neotoma] exit code=${code} uptime=${process.uptime().toFixed(1)}s`);
|
|
9887
|
+
});
|
|
9888
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
9889
|
+
process.on(sig, () => {
|
|
9890
|
+
console.error(`[neotoma] received ${sig} uptime=${process.uptime().toFixed(1)}s`);
|
|
9891
|
+
process.exit(0);
|
|
9892
|
+
});
|
|
9893
|
+
}
|
|
9778
9894
|
startHTTPServer().catch((err) => {
|
|
9779
9895
|
console.error("Failed to start HTTP server:", err);
|
|
9780
9896
|
process.exit(1);
|