@xtrape/capsule-agent-node 0.5.0 → 0.6.1
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/README.md +91 -19
- package/dist/index.cjs +291 -12
- package/dist/index.d.cts +132 -5
- package/dist/index.d.ts +132 -5
- package/dist/index.js +289 -11
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -8,31 +8,28 @@ current Xtrape concept. New integrations should use `XtrapeAgent` and the
|
|
|
8
8
|
`worker` option.
|
|
9
9
|
|
|
10
10
|
[](./LICENSE)
|
|
11
|
-
[](https://forgejo.xtrape.com/xtrape/xtrape-docs)
|
|
12
|
+
[](https://forgejo.xtrape.com/xtrape/xtrape-docs)
|
|
13
13
|
|
|
14
14
|
The package supports the Node.js Embedded Agent mode for one Worker. Multi-Worker
|
|
15
15
|
ownership belongs to a sidecar or external Agent. Gateway, when present, routes
|
|
16
16
|
Agent/control-plane traffic and does not replace the Agent role.
|
|
17
17
|
|
|
18
|
-
> **Package status:**
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
> interfaces may still change.
|
|
18
|
+
> **Package status:** This package follows the unified Xtrape `0.6.0` release
|
|
19
|
+
> train. The npm package name remains a compatibility identifier, but release
|
|
20
|
+
> versioning now follows the shared Xtrape train version.
|
|
22
21
|
|
|
23
22
|
## Install
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
This repository tracks the `0.6.0` snapshot train. Until the `0.6.0` npm cut is
|
|
25
|
+
published, install the latest published compatibility package:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
pnpm add @xtrape/capsule-agent-node
|
|
28
|
+
pnpm add @xtrape/capsule-agent-node@^0.5.0
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
During v0.3 development, this repository may use a local workspace, GitHub
|
|
34
|
-
branch dependency, or `0.3.0-rc.x` package for
|
|
35
|
-
`@xtrape/capsule-contracts-node` until the final `0.3.0` package is published.
|
|
31
|
+
For source-level snapshot integration, use a local checkout of this repository
|
|
32
|
+
and align the rest of the release train separately.
|
|
36
33
|
|
|
37
34
|
For this repository itself:
|
|
38
35
|
|
|
@@ -41,6 +38,78 @@ pnpm install
|
|
|
41
38
|
pnpm build
|
|
42
39
|
```
|
|
43
40
|
|
|
41
|
+
## Xtrape CE Phase 0 Service Agent
|
|
42
|
+
|
|
43
|
+
For the Xtrape CE 0.1 runtime loop (`Telegram → xtrape-server-ce → xtrape-service
|
|
44
|
+
→ reply`), use `CeServiceAgent` (or the `serveCeService` bootstrap). An
|
|
45
|
+
`xtrape-service` registers an instance to `xtrape-server-ce`, heartbeats, and
|
|
46
|
+
replies to `ConversationMessage`s. `Service` and `Worker` are roles here, not the
|
|
47
|
+
legacy `Capsule` concept.
|
|
48
|
+
|
|
49
|
+
### One-call bootstrap
|
|
50
|
+
|
|
51
|
+
`serveCeService` starts an HTTP listener (`POST /messages`, action endpoints,
|
|
52
|
+
`GET /health`),
|
|
53
|
+
registers the instance (sending the registration token if server-ce requires
|
|
54
|
+
one), stores the per-instance access token returned by server-ce, and runs
|
|
55
|
+
authenticated heartbeats — so a service does not copy-paste lifecycle code:
|
|
56
|
+
|
|
57
|
+
For deployed or containerized use, set `address` to the callback URL that
|
|
58
|
+
`xtrape-server-ce` can actually reach and allowlist, for example
|
|
59
|
+
`http://demo-service:8080`. The SDK only derives a default address when you
|
|
60
|
+
bind to an explicit local-only host such as `127.0.0.1`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { serveCeService } from "@xtrape/capsule-agent-node";
|
|
64
|
+
|
|
65
|
+
const svc = await serveCeService({
|
|
66
|
+
serverUrl: process.env.XTRAPE_SERVER_URL ?? "http://localhost:3000",
|
|
67
|
+
registrationToken: process.env.SERVER_CE_REGISTRATION_TOKEN,
|
|
68
|
+
// Optional compatibility fallback for older server-ce deployments. Current
|
|
69
|
+
// server-ce returns a per-instance access token during registration, and the
|
|
70
|
+
// SDK uses that token first for heartbeat/deregister.
|
|
71
|
+
controlToken: process.env.SERVER_CE_CONTROL_TOKEN,
|
|
72
|
+
// Optional: require authenticated callbacks from server-ce on POST /messages
|
|
73
|
+
// and action endpoints (x-xtrape-service-callback-token or Authorization: Bearer).
|
|
74
|
+
callbackToken: process.env.SERVICE_CALLBACK_TOKEN,
|
|
75
|
+
port: 8080,
|
|
76
|
+
// required in deployed/containerized environments
|
|
77
|
+
address: process.env.SERVICE_ADDRESS ?? "http://demo-service:8080",
|
|
78
|
+
manifest: {
|
|
79
|
+
contractVersion: 1,
|
|
80
|
+
service: { id: "demo-echo-service", name: "Demo Echo Service", version: "0.6.0" },
|
|
81
|
+
runtime: { language: "node", agent: "xtrape-agent-nodejs" },
|
|
82
|
+
capabilities: ["status.query"],
|
|
83
|
+
message: { supportedTypes: ["QUERY"] },
|
|
84
|
+
},
|
|
85
|
+
handler: (msg) => `echo: ${msg.content}`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.on("SIGTERM", () => void svc.close()); // stop heartbeat + deregister + close
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Lower-level agent
|
|
92
|
+
|
|
93
|
+
When the service already runs its own HTTP server, use `CeServiceAgent` directly:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { CeServiceAgent } from "@xtrape/capsule-agent-node";
|
|
97
|
+
|
|
98
|
+
const agent = new CeServiceAgent({
|
|
99
|
+
serverUrl: "http://localhost:3000",
|
|
100
|
+
registrationToken: process.env.SERVER_CE_REGISTRATION_TOKEN,
|
|
101
|
+
controlToken: process.env.SERVER_CE_CONTROL_TOKEN, // compatibility fallback only
|
|
102
|
+
address: "http://127.0.0.1:8080",
|
|
103
|
+
manifest: /* ServiceManifest */ undefined as never,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const stop = await agent.start(); // register + heartbeat
|
|
107
|
+
// in your POST /messages route:
|
|
108
|
+
const reply = await agent.handleMessage(requestJson, (m) => `echo: ${m.content}`);
|
|
109
|
+
// on shutdown:
|
|
110
|
+
await stop(); // stop heartbeat + deregister
|
|
111
|
+
```
|
|
112
|
+
|
|
44
113
|
## Minimal Example
|
|
45
114
|
|
|
46
115
|
This example uses the canonical alias `XtrapeAgent`, configures
|
|
@@ -402,10 +471,13 @@ and `logger` are configured, `structuredLogger` wins.
|
|
|
402
471
|
|
|
403
472
|
| Package | Compatible with |
|
|
404
473
|
| ---------------------------------- | ------------------------------------------------------------- |
|
|
474
|
+
| repository line `0.6.0` | current Xtrape `0.6.0` snapshot train |
|
|
405
475
|
| `@xtrape/capsule-agent-node@0.1.x` | Xtrape Panel CE `0.1.x` and `@xtrape/capsule-contracts-node@0.1.x` |
|
|
406
476
|
|
|
407
|
-
Use matching
|
|
408
|
-
|
|
477
|
+
Use matching release-train versions across CE, Agent SDK, and Contracts. During
|
|
478
|
+
the current `0.6.0` snapshot phase, published compatibility packages may lag
|
|
479
|
+
the repository line until the train npm cut is published. The wire protocol may
|
|
480
|
+
still change before `v1.0`.
|
|
409
481
|
|
|
410
482
|
## Troubleshooting
|
|
411
483
|
|
|
@@ -455,10 +527,10 @@ The token is single-use and short-lived.
|
|
|
455
527
|
|
|
456
528
|
## Documentation
|
|
457
529
|
|
|
458
|
-
-
|
|
459
|
-
- Node embedded Agent guide: https://xtrape
|
|
460
|
-
- Action model: https://xtrape
|
|
461
|
-
- Xtrape Panel CE: https://xtrape
|
|
530
|
+
- Public docs repo: https://forgejo.xtrape.com/xtrape/xtrape-site
|
|
531
|
+
- Node embedded Agent guide: https://forgejo.xtrape.com/xtrape/xtrape-site/src/branch/main/docs/agents/node-embedded-agent.md
|
|
532
|
+
- Action model: https://forgejo.xtrape.com/xtrape/xtrape-site/src/branch/main/docs/agents/action-model.md
|
|
533
|
+
- Xtrape Panel CE docs: https://forgejo.xtrape.com/xtrape/xtrape-site/src/branch/main/docs/opstage-ce/overview.md
|
|
462
534
|
|
|
463
535
|
## Contributing
|
|
464
536
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,7 +31,8 @@ __export(index_exports, {
|
|
|
31
31
|
FileTokenStore: () => FileTokenStore,
|
|
32
32
|
NetworkError: () => NetworkError,
|
|
33
33
|
RegistrationError: () => RegistrationError,
|
|
34
|
-
XtrapeAgent: () => CapsuleAgent
|
|
34
|
+
XtrapeAgent: () => CapsuleAgent,
|
|
35
|
+
serveCeService: () => serveCeService
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(index_exports);
|
|
37
38
|
|
|
@@ -431,6 +432,12 @@ var CapsuleAgent = class {
|
|
|
431
432
|
// src/ce/index.ts
|
|
432
433
|
var import_node_crypto = require("crypto");
|
|
433
434
|
var import_capsule_contracts_node = require("@xtrape/capsule-contracts-node");
|
|
435
|
+
var CeInboundMessageValidationError = class extends Error {
|
|
436
|
+
constructor(message, options) {
|
|
437
|
+
super(message, options);
|
|
438
|
+
this.name = "CeInboundMessageValidationError";
|
|
439
|
+
}
|
|
440
|
+
};
|
|
434
441
|
var CeServiceAgent = class {
|
|
435
442
|
serviceId;
|
|
436
443
|
instanceId;
|
|
@@ -438,7 +445,10 @@ var CeServiceAgent = class {
|
|
|
438
445
|
manifest;
|
|
439
446
|
address;
|
|
440
447
|
tenant;
|
|
448
|
+
registrationToken;
|
|
449
|
+
controlToken;
|
|
441
450
|
fetchImpl;
|
|
451
|
+
accessToken;
|
|
442
452
|
health = "HEALTHY";
|
|
443
453
|
hbTimer;
|
|
444
454
|
constructor(opts) {
|
|
@@ -448,8 +458,19 @@ var CeServiceAgent = class {
|
|
|
448
458
|
this.tenant = opts.manifest.tenant ?? "default";
|
|
449
459
|
this.serviceId = opts.manifest.service.id;
|
|
450
460
|
this.instanceId = opts.instanceId ?? `${this.serviceId}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
|
461
|
+
this.registrationToken = opts.registrationToken;
|
|
462
|
+
this.controlToken = opts.controlToken;
|
|
451
463
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
452
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Header carrying the per-instance access token returned by registration.
|
|
467
|
+
* Falls back to the transitional control token for compatibility with older
|
|
468
|
+
* server-ce deployments that did not mint per-instance access tokens yet.
|
|
469
|
+
*/
|
|
470
|
+
lifecycleAuthHeaders() {
|
|
471
|
+
if (this.accessToken) return { "x-xtrape-access-token": this.accessToken };
|
|
472
|
+
return this.controlToken ? { "x-xtrape-control-token": this.controlToken } : void 0;
|
|
473
|
+
}
|
|
453
474
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
454
475
|
instance() {
|
|
455
476
|
return import_capsule_contracts_node.ServiceInstanceSchema.parse({
|
|
@@ -462,10 +483,29 @@ var CeServiceAgent = class {
|
|
|
462
483
|
health: this.health
|
|
463
484
|
});
|
|
464
485
|
}
|
|
465
|
-
/** POST /v1/instances { manifest, instance } */
|
|
486
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
466
487
|
async register() {
|
|
467
|
-
|
|
468
|
-
|
|
488
|
+
const headers = this.registrationToken ? { "x-xtrape-registration-token": this.registrationToken } : void 0;
|
|
489
|
+
const response = await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() }, headers);
|
|
490
|
+
const accessToken = typeof response === "object" && response !== null && "accessToken" in response ? response.accessToken : void 0;
|
|
491
|
+
if (typeof accessToken === "string" && accessToken.length > 0) {
|
|
492
|
+
this.accessToken = accessToken;
|
|
493
|
+
}
|
|
494
|
+
return { instanceId: this.instanceId, ...this.accessToken ? { accessToken: this.accessToken } : {} };
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
498
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
499
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
500
|
+
*/
|
|
501
|
+
async start(opts = {}) {
|
|
502
|
+
await this.register();
|
|
503
|
+
if (opts.heartbeatMs !== 0) this.startHeartbeat(opts.heartbeatMs ?? 1e4, opts.metrics);
|
|
504
|
+
return async () => {
|
|
505
|
+
this.stopHeartbeat();
|
|
506
|
+
await this.deregister().catch(() => {
|
|
507
|
+
});
|
|
508
|
+
};
|
|
469
509
|
}
|
|
470
510
|
setHealth(h) {
|
|
471
511
|
this.health = h;
|
|
@@ -481,7 +521,7 @@ var CeServiceAgent = class {
|
|
|
481
521
|
health: this.health,
|
|
482
522
|
...metricsSummary ? { metricsSummary } : {}
|
|
483
523
|
};
|
|
484
|
-
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
|
|
524
|
+
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb, this.lifecycleAuthHeaders());
|
|
485
525
|
}
|
|
486
526
|
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
487
527
|
startHeartbeat(intervalMs = 1e4, metrics) {
|
|
@@ -501,14 +541,25 @@ var CeServiceAgent = class {
|
|
|
501
541
|
}
|
|
502
542
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
503
543
|
async deregister() {
|
|
504
|
-
await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, {
|
|
544
|
+
await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, {
|
|
545
|
+
method: "DELETE",
|
|
546
|
+
...this.lifecycleAuthHeaders() ? { headers: this.lifecycleAuthHeaders() } : {}
|
|
547
|
+
});
|
|
505
548
|
}
|
|
506
549
|
/**
|
|
507
550
|
* Validate an inbound message, run the handler, and build the reply message
|
|
508
551
|
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
509
552
|
*/
|
|
510
553
|
async handleMessage(raw, handler) {
|
|
511
|
-
|
|
554
|
+
let msg;
|
|
555
|
+
try {
|
|
556
|
+
msg = import_capsule_contracts_node.ConversationMessageSchema.parse(raw);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
throw new CeInboundMessageValidationError(
|
|
559
|
+
error instanceof Error ? error.message : "invalid inbound ConversationMessage",
|
|
560
|
+
{ cause: error }
|
|
561
|
+
);
|
|
562
|
+
}
|
|
512
563
|
const out = await handler(msg);
|
|
513
564
|
const reply = typeof out === "string" ? { content: out } : out;
|
|
514
565
|
return import_capsule_contracts_node.ConversationMessageSchema.parse({
|
|
@@ -527,8 +578,8 @@ var CeServiceAgent = class {
|
|
|
527
578
|
...reply.payload ? { payload: reply.payload } : {}
|
|
528
579
|
});
|
|
529
580
|
}
|
|
530
|
-
post(path, body) {
|
|
531
|
-
return this.fetchJson(path, { method: "POST", body: JSON.stringify(body) });
|
|
581
|
+
post(path, body, headers) {
|
|
582
|
+
return this.fetchJson(path, { method: "POST", body: JSON.stringify(body), ...headers ? { headers } : {} });
|
|
532
583
|
}
|
|
533
584
|
async fetchJson(path, init) {
|
|
534
585
|
const res = await this.fetchImpl(this.serverUrl + path, {
|
|
@@ -536,13 +587,240 @@ var CeServiceAgent = class {
|
|
|
536
587
|
headers: { "content-type": "application/json", ...init.headers ?? {} }
|
|
537
588
|
});
|
|
538
589
|
const text = await res.text();
|
|
539
|
-
const data = text ? JSON.parse(text) : void 0;
|
|
540
590
|
if (!res.ok) {
|
|
541
591
|
throw new Error(`server-ce ${path} -> ${res.status}: ${text}`);
|
|
542
592
|
}
|
|
543
|
-
return
|
|
593
|
+
if (!text) return void 0;
|
|
594
|
+
try {
|
|
595
|
+
return JSON.parse(text);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
throw new Error(`server-ce ${path} returned non-JSON success response: ${text}`, { cause: error });
|
|
598
|
+
}
|
|
544
599
|
}
|
|
545
600
|
};
|
|
601
|
+
|
|
602
|
+
// src/ce/serve.ts
|
|
603
|
+
var import_node_http = require("http");
|
|
604
|
+
function send(res, status, body) {
|
|
605
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
606
|
+
res.end(JSON.stringify(body));
|
|
607
|
+
}
|
|
608
|
+
function readBody(req) {
|
|
609
|
+
return new Promise((resolve, reject) => {
|
|
610
|
+
let data = "";
|
|
611
|
+
req.on("data", (c) => data += c);
|
|
612
|
+
req.on("end", () => resolve(data));
|
|
613
|
+
req.on("error", reject);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function isLocalOnlyHost(host) {
|
|
617
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
618
|
+
}
|
|
619
|
+
function extractCallbackToken(req) {
|
|
620
|
+
const raw = req.headers["x-xtrape-service-callback-token"];
|
|
621
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
622
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
623
|
+
const auth = req.headers["authorization"];
|
|
624
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
625
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
626
|
+
return token.length > 0 ? token : void 0;
|
|
627
|
+
}
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
function requireCallbackAuthorized(req, res, callbackToken) {
|
|
631
|
+
if (!callbackToken || extractCallbackToken(req) === callbackToken) return true;
|
|
632
|
+
send(res, 401, {
|
|
633
|
+
code: "UNAUTHORIZED",
|
|
634
|
+
message: "service callback token is missing or invalid",
|
|
635
|
+
retryable: false
|
|
636
|
+
});
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
function defaultLocalAddress(host, port) {
|
|
640
|
+
if (host === "::1") return `http://[::1]:${port}`;
|
|
641
|
+
return `http://${host}:${port}`;
|
|
642
|
+
}
|
|
643
|
+
function listen(server, port, host) {
|
|
644
|
+
return new Promise((resolve, reject) => {
|
|
645
|
+
const onError = (error) => {
|
|
646
|
+
server.off("listening", onListening);
|
|
647
|
+
reject(error);
|
|
648
|
+
};
|
|
649
|
+
const onListening = () => {
|
|
650
|
+
server.off("error", onError);
|
|
651
|
+
resolve();
|
|
652
|
+
};
|
|
653
|
+
server.once("error", onError);
|
|
654
|
+
server.once("listening", onListening);
|
|
655
|
+
server.listen(port, host);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
function sendMessageError(res, error) {
|
|
659
|
+
if (error instanceof CeInboundMessageValidationError) {
|
|
660
|
+
send(res, 400, { code: "VALIDATION_FAILED", message: error.message, retryable: false });
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const message = error instanceof Error ? error.message : "message handler failed";
|
|
664
|
+
send(res, 500, { code: "HANDLER_FAILED", message, retryable: true });
|
|
665
|
+
}
|
|
666
|
+
function sendHandlerError(res, error) {
|
|
667
|
+
const message = error instanceof Error ? error.message : "action handler failed";
|
|
668
|
+
send(res, 500, { code: "ACTION_FAILED", message, retryable: false });
|
|
669
|
+
}
|
|
670
|
+
async function serveCeService(opts) {
|
|
671
|
+
const host = opts.host ?? "0.0.0.0";
|
|
672
|
+
let agent;
|
|
673
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
674
|
+
void (async () => {
|
|
675
|
+
try {
|
|
676
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
677
|
+
send(res, 200, { ok: true, service: opts.manifest.service.id });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (req.method === "POST" && req.url === "/messages") {
|
|
681
|
+
if (!agent) {
|
|
682
|
+
send(res, 503, { code: "NOT_READY", message: "service not registered yet", retryable: true });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
686
|
+
const raw = await readBody(req);
|
|
687
|
+
let payload;
|
|
688
|
+
try {
|
|
689
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
690
|
+
} catch (error) {
|
|
691
|
+
send(res, 400, {
|
|
692
|
+
code: "VALIDATION_FAILED",
|
|
693
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
694
|
+
retryable: false
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const reply = await agent.handleMessage(payload, opts.handler);
|
|
699
|
+
send(res, 200, reply);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (req.method === "GET" && req.url === "/actions") {
|
|
703
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
704
|
+
send(
|
|
705
|
+
res,
|
|
706
|
+
200,
|
|
707
|
+
(opts.actions ?? []).map((action) => ({
|
|
708
|
+
name: action.name,
|
|
709
|
+
label: action.label,
|
|
710
|
+
description: action.description,
|
|
711
|
+
category: action.category,
|
|
712
|
+
order: action.order,
|
|
713
|
+
requiresPrepare: typeof action.prepare === "function" ? true : void 0,
|
|
714
|
+
inputSchema: action.inputSchema
|
|
715
|
+
}))
|
|
716
|
+
);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const prepareMatch = req.url?.match(/^\/actions\/([^/]+)\/prepare$/);
|
|
720
|
+
if (req.method === "POST" && prepareMatch) {
|
|
721
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
722
|
+
const name = decodeURIComponent(prepareMatch[1]);
|
|
723
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
724
|
+
if (!action) {
|
|
725
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (typeof action.prepare !== "function") {
|
|
729
|
+
send(res, 400, { code: "ACTION_NOT_PREPARABLE", message: `action ${name} has no prepare step`, retryable: false });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const raw = await readBody(req);
|
|
733
|
+
let payload;
|
|
734
|
+
try {
|
|
735
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
736
|
+
} catch (error) {
|
|
737
|
+
send(res, 400, {
|
|
738
|
+
code: "VALIDATION_FAILED",
|
|
739
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
740
|
+
retryable: false
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
send(res, 200, await action.prepare(payload) ?? {});
|
|
746
|
+
} catch (error) {
|
|
747
|
+
sendHandlerError(res, error);
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const executeMatch = req.url?.match(/^\/actions\/([^/]+)\/execute$/);
|
|
752
|
+
if (req.method === "POST" && executeMatch) {
|
|
753
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
754
|
+
const name = decodeURIComponent(executeMatch[1]);
|
|
755
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
756
|
+
if (!action) {
|
|
757
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const raw = await readBody(req);
|
|
761
|
+
let payload;
|
|
762
|
+
try {
|
|
763
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
764
|
+
} catch (error) {
|
|
765
|
+
send(res, 400, {
|
|
766
|
+
code: "VALIDATION_FAILED",
|
|
767
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
768
|
+
retryable: false
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
send(res, 200, await action.execute(payload));
|
|
774
|
+
} catch (error) {
|
|
775
|
+
sendHandlerError(res, error);
|
|
776
|
+
}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
send(res, 404, { code: "NOT_FOUND", message: "not found", retryable: false });
|
|
780
|
+
} catch (e) {
|
|
781
|
+
sendMessageError(res, e);
|
|
782
|
+
}
|
|
783
|
+
})();
|
|
784
|
+
});
|
|
785
|
+
await listen(server, opts.port ?? 0, host);
|
|
786
|
+
const info = server.address();
|
|
787
|
+
const port = typeof info === "object" && info ? info.port : opts.port ?? 0;
|
|
788
|
+
const address = opts.address ?? (isLocalOnlyHost(host) ? defaultLocalAddress(host, port) : void 0);
|
|
789
|
+
if (!address) {
|
|
790
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
791
|
+
throw new Error(
|
|
792
|
+
`serveCeService requires an explicit address when host=${host}. For deployed/containerized use, pass a reachable callback URL such as http://demo-service:8080.`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
agent = new CeServiceAgent({
|
|
797
|
+
serverUrl: opts.serverUrl,
|
|
798
|
+
manifest: opts.manifest,
|
|
799
|
+
address,
|
|
800
|
+
instanceId: opts.instanceId,
|
|
801
|
+
registrationToken: opts.registrationToken,
|
|
802
|
+
controlToken: opts.controlToken,
|
|
803
|
+
fetchImpl: opts.fetchImpl
|
|
804
|
+
});
|
|
805
|
+
await agent.register();
|
|
806
|
+
if (opts.heartbeatMs !== 0) agent.startHeartbeat(opts.heartbeatMs ?? 1e4);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
agent?.stopHeartbeat();
|
|
809
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
agent,
|
|
814
|
+
address,
|
|
815
|
+
port,
|
|
816
|
+
async close() {
|
|
817
|
+
agent?.stopHeartbeat();
|
|
818
|
+
await agent?.deregister().catch(() => {
|
|
819
|
+
});
|
|
820
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
546
824
|
// Annotate the CommonJS export names for ESM import in node:
|
|
547
825
|
0 && (module.exports = {
|
|
548
826
|
AgentApiClient,
|
|
@@ -556,5 +834,6 @@ var CeServiceAgent = class {
|
|
|
556
834
|
FileTokenStore,
|
|
557
835
|
NetworkError,
|
|
558
836
|
RegistrationError,
|
|
559
|
-
XtrapeAgent
|
|
837
|
+
XtrapeAgent,
|
|
838
|
+
serveCeService
|
|
560
839
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -277,9 +277,27 @@ interface CeServiceAgentOptions {
|
|
|
277
277
|
address: string;
|
|
278
278
|
/** optional fixed instance id; defaults to `<serviceId>-<rand>` */
|
|
279
279
|
instanceId?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Shared registration token. When server-ce has `SERVER_CE_REGISTRATION_TOKEN`
|
|
282
|
+
* set, registration requires it; the agent sends it as the
|
|
283
|
+
* `x-xtrape-registration-token` header on register.
|
|
284
|
+
*/
|
|
285
|
+
registrationToken?: string;
|
|
286
|
+
/**
|
|
287
|
+
* 0.6.x control-plane token. When server-ce has `SERVER_CE_CONTROL_TOKEN` set,
|
|
288
|
+
* older non-registration routes may require it. Current server-ce returns a
|
|
289
|
+
* per-instance access token during registration and the agent uses that token
|
|
290
|
+
* for heartbeat/deregister; this control token is a compatibility fallback
|
|
291
|
+
* (see xtrape-server-ce#12).
|
|
292
|
+
*/
|
|
293
|
+
controlToken?: string;
|
|
280
294
|
/** injectable fetch (tests); defaults to global fetch */
|
|
281
295
|
fetchImpl?: typeof fetch;
|
|
282
296
|
}
|
|
297
|
+
interface CeServiceRegistrationResult {
|
|
298
|
+
instanceId: string;
|
|
299
|
+
accessToken?: string;
|
|
300
|
+
}
|
|
283
301
|
type CeMessageReply = string | {
|
|
284
302
|
content: string;
|
|
285
303
|
messageType?: ConversationMessage["messageType"];
|
|
@@ -294,16 +312,32 @@ declare class CeServiceAgent {
|
|
|
294
312
|
private readonly manifest;
|
|
295
313
|
private readonly address;
|
|
296
314
|
private readonly tenant;
|
|
315
|
+
private readonly registrationToken?;
|
|
316
|
+
private readonly controlToken?;
|
|
297
317
|
private readonly fetchImpl;
|
|
318
|
+
private accessToken?;
|
|
298
319
|
private health;
|
|
299
320
|
private hbTimer?;
|
|
300
321
|
constructor(opts: CeServiceAgentOptions);
|
|
322
|
+
/**
|
|
323
|
+
* Header carrying the per-instance access token returned by registration.
|
|
324
|
+
* Falls back to the transitional control token for compatibility with older
|
|
325
|
+
* server-ce deployments that did not mint per-instance access tokens yet.
|
|
326
|
+
*/
|
|
327
|
+
private lifecycleAuthHeaders;
|
|
301
328
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
302
329
|
instance(): ServiceInstance;
|
|
303
|
-
/** POST /v1/instances { manifest, instance } */
|
|
304
|
-
register(): Promise<
|
|
305
|
-
|
|
306
|
-
|
|
330
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
331
|
+
register(): Promise<CeServiceRegistrationResult>;
|
|
332
|
+
/**
|
|
333
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
334
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
335
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
336
|
+
*/
|
|
337
|
+
start(opts?: {
|
|
338
|
+
heartbeatMs?: number;
|
|
339
|
+
metrics?: () => Record<string, unknown>;
|
|
340
|
+
}): Promise<() => Promise<void>>;
|
|
307
341
|
setHealth(h: ServiceInstanceHealth): void;
|
|
308
342
|
/** POST /v1/instances/{id}/heartbeat */
|
|
309
343
|
sendHeartbeat(metricsSummary?: Record<string, unknown>): Promise<void>;
|
|
@@ -321,4 +355,97 @@ declare class CeServiceAgent {
|
|
|
321
355
|
private fetchJson;
|
|
322
356
|
}
|
|
323
357
|
|
|
324
|
-
|
|
358
|
+
/**
|
|
359
|
+
* One-call bootstrap for an `xtrape-service`: start an HTTP listener that serves
|
|
360
|
+
* `POST /messages`, action endpoints (and `GET /health`), register the instance to xtrape-server-ce,
|
|
361
|
+
* and run heartbeats. Returns a handle whose `close()` stops heartbeats,
|
|
362
|
+
* deregisters, and closes the listener.
|
|
363
|
+
*
|
|
364
|
+
* Uses only `node:http` so it stays framework-agnostic. A service that already
|
|
365
|
+
* has an HTTP server can use `CeServiceAgent` directly instead.
|
|
366
|
+
*/
|
|
367
|
+
interface ServeCeServiceOptions {
|
|
368
|
+
/** xtrape-server-ce base URL, e.g. http://localhost:3000 */
|
|
369
|
+
serverUrl: string;
|
|
370
|
+
manifest: ServiceManifest;
|
|
371
|
+
/** business handler invoked per inbound ConversationMessage */
|
|
372
|
+
handler: CeMessageHandler;
|
|
373
|
+
/** Optional CE action handlers exposed over GET /actions, POST /actions/:name/prepare, and POST /actions/:name/execute. */
|
|
374
|
+
actions?: CeServiceActionDefinition[];
|
|
375
|
+
/** shared registration token, if server-ce requires one */
|
|
376
|
+
registrationToken?: string;
|
|
377
|
+
/**
|
|
378
|
+
* 0.6.x control-plane token forwarded to server-ce on heartbeat/deregister
|
|
379
|
+
* only as a compatibility fallback. Current server-ce returns a per-instance
|
|
380
|
+
* access token during registration and the SDK uses that first.
|
|
381
|
+
*/
|
|
382
|
+
controlToken?: string;
|
|
383
|
+
/**
|
|
384
|
+
* Optional inbound callback token. When set, `POST /messages` and action
|
|
385
|
+
* endpoints (`GET /actions`, `POST /actions/:name/prepare`,
|
|
386
|
+
* `POST /actions/:name/execute`) must include
|
|
387
|
+
* `x-xtrape-service-callback-token: <callbackToken>` (or
|
|
388
|
+
* `Authorization: Bearer <callbackToken>`); otherwise the request is rejected
|
|
389
|
+
* with 401. Lets a service require authenticated callbacks from server-ce.
|
|
390
|
+
*/
|
|
391
|
+
callbackToken?: string;
|
|
392
|
+
/** listen port; 0 picks an ephemeral port (default 0) */
|
|
393
|
+
port?: number;
|
|
394
|
+
/** listen host (default 0.0.0.0) */
|
|
395
|
+
host?: string;
|
|
396
|
+
/**
|
|
397
|
+
* Address server-ce should call back.
|
|
398
|
+
*
|
|
399
|
+
* Required for deployed/containerized use. When omitted, the SDK only derives
|
|
400
|
+
* a default address for explicit local-only hosts such as `127.0.0.1` or
|
|
401
|
+
* `localhost`.
|
|
402
|
+
*/
|
|
403
|
+
address?: string;
|
|
404
|
+
/** heartbeat interval ms; 0 disables (default 10000) */
|
|
405
|
+
heartbeatMs?: number;
|
|
406
|
+
instanceId?: string;
|
|
407
|
+
/** injectable fetch (tests); defaults to global fetch */
|
|
408
|
+
fetchImpl?: typeof fetch;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* What a two-phase action's prepare step returns to the operator UI:
|
|
412
|
+
* `initialPayload` seeds the execute form, `currentState` is contextual
|
|
413
|
+
* data to display (e.g. the accounts/balances the decision is based on),
|
|
414
|
+
* and `inputSchema` optionally overrides the static schema with live options.
|
|
415
|
+
*/
|
|
416
|
+
interface CeActionPrepareResult {
|
|
417
|
+
/** Longer operator-facing explanation shown on the execution screen. */
|
|
418
|
+
longDescription?: string;
|
|
419
|
+
initialPayload?: Record<string, unknown>;
|
|
420
|
+
currentState?: Record<string, unknown>;
|
|
421
|
+
inputSchema?: Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
interface CeServiceActionDefinition {
|
|
424
|
+
name: string;
|
|
425
|
+
label: string;
|
|
426
|
+
description?: string;
|
|
427
|
+
/** Operator-facing grouping shown by Panel (capsule ActionDefinition heritage), e.g. "diagnostics". */
|
|
428
|
+
category?: string;
|
|
429
|
+
/** Display order within the category; lower sorts first. */
|
|
430
|
+
order?: number;
|
|
431
|
+
inputSchema?: Record<string, unknown>;
|
|
432
|
+
/**
|
|
433
|
+
* Optional prepare step. When present the action is two-phase: server-ce
|
|
434
|
+
* calls this before the operator fills the form to fetch live defaults and
|
|
435
|
+
* current state. Advertised as `requiresPrepare: true` in the action list.
|
|
436
|
+
*/
|
|
437
|
+
prepare?(input?: unknown): CeActionPrepareResult | Promise<CeActionPrepareResult>;
|
|
438
|
+
execute(input: unknown): unknown | Promise<unknown>;
|
|
439
|
+
}
|
|
440
|
+
interface ServeCeServiceHandle {
|
|
441
|
+
agent: CeServiceAgent;
|
|
442
|
+
/** advertised address registered with server-ce */
|
|
443
|
+
address: string;
|
|
444
|
+
/** actual listening port */
|
|
445
|
+
port: number;
|
|
446
|
+
/** stop heartbeats, deregister (best-effort), and close the HTTP server */
|
|
447
|
+
close(): Promise<void>;
|
|
448
|
+
}
|
|
449
|
+
declare function serveCeService(opts: ServeCeServiceOptions): Promise<ServeCeServiceHandle>;
|
|
450
|
+
|
|
451
|
+
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type CeActionPrepareResult, type CeMessageHandler, type CeMessageReply, type CeServiceActionDefinition, CeServiceAgent, type CeServiceAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, type LegacyServiceAgentOptions, NetworkError, type PublishBusEventInput, type PublishBusEventResult, type RegisteredAction, RegistrationError, type ServeCeServiceHandle, type ServeCeServiceOptions, type ServiceSnapshot, type StructuredLogger, type TokenStore, type WorkerAgentOptions, type WorkerDefinition, type WorkerSnapshot, CapsuleAgent as XtrapeAgent, type XtrapeAgentOptions, serveCeService };
|
package/dist/index.d.ts
CHANGED
|
@@ -277,9 +277,27 @@ interface CeServiceAgentOptions {
|
|
|
277
277
|
address: string;
|
|
278
278
|
/** optional fixed instance id; defaults to `<serviceId>-<rand>` */
|
|
279
279
|
instanceId?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Shared registration token. When server-ce has `SERVER_CE_REGISTRATION_TOKEN`
|
|
282
|
+
* set, registration requires it; the agent sends it as the
|
|
283
|
+
* `x-xtrape-registration-token` header on register.
|
|
284
|
+
*/
|
|
285
|
+
registrationToken?: string;
|
|
286
|
+
/**
|
|
287
|
+
* 0.6.x control-plane token. When server-ce has `SERVER_CE_CONTROL_TOKEN` set,
|
|
288
|
+
* older non-registration routes may require it. Current server-ce returns a
|
|
289
|
+
* per-instance access token during registration and the agent uses that token
|
|
290
|
+
* for heartbeat/deregister; this control token is a compatibility fallback
|
|
291
|
+
* (see xtrape-server-ce#12).
|
|
292
|
+
*/
|
|
293
|
+
controlToken?: string;
|
|
280
294
|
/** injectable fetch (tests); defaults to global fetch */
|
|
281
295
|
fetchImpl?: typeof fetch;
|
|
282
296
|
}
|
|
297
|
+
interface CeServiceRegistrationResult {
|
|
298
|
+
instanceId: string;
|
|
299
|
+
accessToken?: string;
|
|
300
|
+
}
|
|
283
301
|
type CeMessageReply = string | {
|
|
284
302
|
content: string;
|
|
285
303
|
messageType?: ConversationMessage["messageType"];
|
|
@@ -294,16 +312,32 @@ declare class CeServiceAgent {
|
|
|
294
312
|
private readonly manifest;
|
|
295
313
|
private readonly address;
|
|
296
314
|
private readonly tenant;
|
|
315
|
+
private readonly registrationToken?;
|
|
316
|
+
private readonly controlToken?;
|
|
297
317
|
private readonly fetchImpl;
|
|
318
|
+
private accessToken?;
|
|
298
319
|
private health;
|
|
299
320
|
private hbTimer?;
|
|
300
321
|
constructor(opts: CeServiceAgentOptions);
|
|
322
|
+
/**
|
|
323
|
+
* Header carrying the per-instance access token returned by registration.
|
|
324
|
+
* Falls back to the transitional control token for compatibility with older
|
|
325
|
+
* server-ce deployments that did not mint per-instance access tokens yet.
|
|
326
|
+
*/
|
|
327
|
+
private lifecycleAuthHeaders;
|
|
301
328
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
302
329
|
instance(): ServiceInstance;
|
|
303
|
-
/** POST /v1/instances { manifest, instance } */
|
|
304
|
-
register(): Promise<
|
|
305
|
-
|
|
306
|
-
|
|
330
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
331
|
+
register(): Promise<CeServiceRegistrationResult>;
|
|
332
|
+
/**
|
|
333
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
334
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
335
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
336
|
+
*/
|
|
337
|
+
start(opts?: {
|
|
338
|
+
heartbeatMs?: number;
|
|
339
|
+
metrics?: () => Record<string, unknown>;
|
|
340
|
+
}): Promise<() => Promise<void>>;
|
|
307
341
|
setHealth(h: ServiceInstanceHealth): void;
|
|
308
342
|
/** POST /v1/instances/{id}/heartbeat */
|
|
309
343
|
sendHeartbeat(metricsSummary?: Record<string, unknown>): Promise<void>;
|
|
@@ -321,4 +355,97 @@ declare class CeServiceAgent {
|
|
|
321
355
|
private fetchJson;
|
|
322
356
|
}
|
|
323
357
|
|
|
324
|
-
|
|
358
|
+
/**
|
|
359
|
+
* One-call bootstrap for an `xtrape-service`: start an HTTP listener that serves
|
|
360
|
+
* `POST /messages`, action endpoints (and `GET /health`), register the instance to xtrape-server-ce,
|
|
361
|
+
* and run heartbeats. Returns a handle whose `close()` stops heartbeats,
|
|
362
|
+
* deregisters, and closes the listener.
|
|
363
|
+
*
|
|
364
|
+
* Uses only `node:http` so it stays framework-agnostic. A service that already
|
|
365
|
+
* has an HTTP server can use `CeServiceAgent` directly instead.
|
|
366
|
+
*/
|
|
367
|
+
interface ServeCeServiceOptions {
|
|
368
|
+
/** xtrape-server-ce base URL, e.g. http://localhost:3000 */
|
|
369
|
+
serverUrl: string;
|
|
370
|
+
manifest: ServiceManifest;
|
|
371
|
+
/** business handler invoked per inbound ConversationMessage */
|
|
372
|
+
handler: CeMessageHandler;
|
|
373
|
+
/** Optional CE action handlers exposed over GET /actions, POST /actions/:name/prepare, and POST /actions/:name/execute. */
|
|
374
|
+
actions?: CeServiceActionDefinition[];
|
|
375
|
+
/** shared registration token, if server-ce requires one */
|
|
376
|
+
registrationToken?: string;
|
|
377
|
+
/**
|
|
378
|
+
* 0.6.x control-plane token forwarded to server-ce on heartbeat/deregister
|
|
379
|
+
* only as a compatibility fallback. Current server-ce returns a per-instance
|
|
380
|
+
* access token during registration and the SDK uses that first.
|
|
381
|
+
*/
|
|
382
|
+
controlToken?: string;
|
|
383
|
+
/**
|
|
384
|
+
* Optional inbound callback token. When set, `POST /messages` and action
|
|
385
|
+
* endpoints (`GET /actions`, `POST /actions/:name/prepare`,
|
|
386
|
+
* `POST /actions/:name/execute`) must include
|
|
387
|
+
* `x-xtrape-service-callback-token: <callbackToken>` (or
|
|
388
|
+
* `Authorization: Bearer <callbackToken>`); otherwise the request is rejected
|
|
389
|
+
* with 401. Lets a service require authenticated callbacks from server-ce.
|
|
390
|
+
*/
|
|
391
|
+
callbackToken?: string;
|
|
392
|
+
/** listen port; 0 picks an ephemeral port (default 0) */
|
|
393
|
+
port?: number;
|
|
394
|
+
/** listen host (default 0.0.0.0) */
|
|
395
|
+
host?: string;
|
|
396
|
+
/**
|
|
397
|
+
* Address server-ce should call back.
|
|
398
|
+
*
|
|
399
|
+
* Required for deployed/containerized use. When omitted, the SDK only derives
|
|
400
|
+
* a default address for explicit local-only hosts such as `127.0.0.1` or
|
|
401
|
+
* `localhost`.
|
|
402
|
+
*/
|
|
403
|
+
address?: string;
|
|
404
|
+
/** heartbeat interval ms; 0 disables (default 10000) */
|
|
405
|
+
heartbeatMs?: number;
|
|
406
|
+
instanceId?: string;
|
|
407
|
+
/** injectable fetch (tests); defaults to global fetch */
|
|
408
|
+
fetchImpl?: typeof fetch;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* What a two-phase action's prepare step returns to the operator UI:
|
|
412
|
+
* `initialPayload` seeds the execute form, `currentState` is contextual
|
|
413
|
+
* data to display (e.g. the accounts/balances the decision is based on),
|
|
414
|
+
* and `inputSchema` optionally overrides the static schema with live options.
|
|
415
|
+
*/
|
|
416
|
+
interface CeActionPrepareResult {
|
|
417
|
+
/** Longer operator-facing explanation shown on the execution screen. */
|
|
418
|
+
longDescription?: string;
|
|
419
|
+
initialPayload?: Record<string, unknown>;
|
|
420
|
+
currentState?: Record<string, unknown>;
|
|
421
|
+
inputSchema?: Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
interface CeServiceActionDefinition {
|
|
424
|
+
name: string;
|
|
425
|
+
label: string;
|
|
426
|
+
description?: string;
|
|
427
|
+
/** Operator-facing grouping shown by Panel (capsule ActionDefinition heritage), e.g. "diagnostics". */
|
|
428
|
+
category?: string;
|
|
429
|
+
/** Display order within the category; lower sorts first. */
|
|
430
|
+
order?: number;
|
|
431
|
+
inputSchema?: Record<string, unknown>;
|
|
432
|
+
/**
|
|
433
|
+
* Optional prepare step. When present the action is two-phase: server-ce
|
|
434
|
+
* calls this before the operator fills the form to fetch live defaults and
|
|
435
|
+
* current state. Advertised as `requiresPrepare: true` in the action list.
|
|
436
|
+
*/
|
|
437
|
+
prepare?(input?: unknown): CeActionPrepareResult | Promise<CeActionPrepareResult>;
|
|
438
|
+
execute(input: unknown): unknown | Promise<unknown>;
|
|
439
|
+
}
|
|
440
|
+
interface ServeCeServiceHandle {
|
|
441
|
+
agent: CeServiceAgent;
|
|
442
|
+
/** advertised address registered with server-ce */
|
|
443
|
+
address: string;
|
|
444
|
+
/** actual listening port */
|
|
445
|
+
port: number;
|
|
446
|
+
/** stop heartbeats, deregister (best-effort), and close the HTTP server */
|
|
447
|
+
close(): Promise<void>;
|
|
448
|
+
}
|
|
449
|
+
declare function serveCeService(opts: ServeCeServiceOptions): Promise<ServeCeServiceHandle>;
|
|
450
|
+
|
|
451
|
+
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type CeActionPrepareResult, type CeMessageHandler, type CeMessageReply, type CeServiceActionDefinition, CeServiceAgent, type CeServiceAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, type LegacyServiceAgentOptions, NetworkError, type PublishBusEventInput, type PublishBusEventResult, type RegisteredAction, RegistrationError, type ServeCeServiceHandle, type ServeCeServiceOptions, type ServiceSnapshot, type StructuredLogger, type TokenStore, type WorkerAgentOptions, type WorkerDefinition, type WorkerSnapshot, CapsuleAgent as XtrapeAgent, type XtrapeAgentOptions, serveCeService };
|
package/dist/index.js
CHANGED
|
@@ -398,6 +398,12 @@ import {
|
|
|
398
398
|
ServiceInstanceSchema,
|
|
399
399
|
ConversationMessageSchema
|
|
400
400
|
} from "@xtrape/capsule-contracts-node";
|
|
401
|
+
var CeInboundMessageValidationError = class extends Error {
|
|
402
|
+
constructor(message, options) {
|
|
403
|
+
super(message, options);
|
|
404
|
+
this.name = "CeInboundMessageValidationError";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
401
407
|
var CeServiceAgent = class {
|
|
402
408
|
serviceId;
|
|
403
409
|
instanceId;
|
|
@@ -405,7 +411,10 @@ var CeServiceAgent = class {
|
|
|
405
411
|
manifest;
|
|
406
412
|
address;
|
|
407
413
|
tenant;
|
|
414
|
+
registrationToken;
|
|
415
|
+
controlToken;
|
|
408
416
|
fetchImpl;
|
|
417
|
+
accessToken;
|
|
409
418
|
health = "HEALTHY";
|
|
410
419
|
hbTimer;
|
|
411
420
|
constructor(opts) {
|
|
@@ -415,8 +424,19 @@ var CeServiceAgent = class {
|
|
|
415
424
|
this.tenant = opts.manifest.tenant ?? "default";
|
|
416
425
|
this.serviceId = opts.manifest.service.id;
|
|
417
426
|
this.instanceId = opts.instanceId ?? `${this.serviceId}-${randomUUID().slice(0, 8)}`;
|
|
427
|
+
this.registrationToken = opts.registrationToken;
|
|
428
|
+
this.controlToken = opts.controlToken;
|
|
418
429
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
419
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Header carrying the per-instance access token returned by registration.
|
|
433
|
+
* Falls back to the transitional control token for compatibility with older
|
|
434
|
+
* server-ce deployments that did not mint per-instance access tokens yet.
|
|
435
|
+
*/
|
|
436
|
+
lifecycleAuthHeaders() {
|
|
437
|
+
if (this.accessToken) return { "x-xtrape-access-token": this.accessToken };
|
|
438
|
+
return this.controlToken ? { "x-xtrape-control-token": this.controlToken } : void 0;
|
|
439
|
+
}
|
|
420
440
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
421
441
|
instance() {
|
|
422
442
|
return ServiceInstanceSchema.parse({
|
|
@@ -429,10 +449,29 @@ var CeServiceAgent = class {
|
|
|
429
449
|
health: this.health
|
|
430
450
|
});
|
|
431
451
|
}
|
|
432
|
-
/** POST /v1/instances { manifest, instance } */
|
|
452
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
433
453
|
async register() {
|
|
434
|
-
|
|
435
|
-
|
|
454
|
+
const headers = this.registrationToken ? { "x-xtrape-registration-token": this.registrationToken } : void 0;
|
|
455
|
+
const response = await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() }, headers);
|
|
456
|
+
const accessToken = typeof response === "object" && response !== null && "accessToken" in response ? response.accessToken : void 0;
|
|
457
|
+
if (typeof accessToken === "string" && accessToken.length > 0) {
|
|
458
|
+
this.accessToken = accessToken;
|
|
459
|
+
}
|
|
460
|
+
return { instanceId: this.instanceId, ...this.accessToken ? { accessToken: this.accessToken } : {} };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
464
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
465
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
466
|
+
*/
|
|
467
|
+
async start(opts = {}) {
|
|
468
|
+
await this.register();
|
|
469
|
+
if (opts.heartbeatMs !== 0) this.startHeartbeat(opts.heartbeatMs ?? 1e4, opts.metrics);
|
|
470
|
+
return async () => {
|
|
471
|
+
this.stopHeartbeat();
|
|
472
|
+
await this.deregister().catch(() => {
|
|
473
|
+
});
|
|
474
|
+
};
|
|
436
475
|
}
|
|
437
476
|
setHealth(h) {
|
|
438
477
|
this.health = h;
|
|
@@ -448,7 +487,7 @@ var CeServiceAgent = class {
|
|
|
448
487
|
health: this.health,
|
|
449
488
|
...metricsSummary ? { metricsSummary } : {}
|
|
450
489
|
};
|
|
451
|
-
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
|
|
490
|
+
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb, this.lifecycleAuthHeaders());
|
|
452
491
|
}
|
|
453
492
|
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
454
493
|
startHeartbeat(intervalMs = 1e4, metrics) {
|
|
@@ -468,14 +507,25 @@ var CeServiceAgent = class {
|
|
|
468
507
|
}
|
|
469
508
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
470
509
|
async deregister() {
|
|
471
|
-
await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, {
|
|
510
|
+
await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, {
|
|
511
|
+
method: "DELETE",
|
|
512
|
+
...this.lifecycleAuthHeaders() ? { headers: this.lifecycleAuthHeaders() } : {}
|
|
513
|
+
});
|
|
472
514
|
}
|
|
473
515
|
/**
|
|
474
516
|
* Validate an inbound message, run the handler, and build the reply message
|
|
475
517
|
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
476
518
|
*/
|
|
477
519
|
async handleMessage(raw, handler) {
|
|
478
|
-
|
|
520
|
+
let msg;
|
|
521
|
+
try {
|
|
522
|
+
msg = ConversationMessageSchema.parse(raw);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
throw new CeInboundMessageValidationError(
|
|
525
|
+
error instanceof Error ? error.message : "invalid inbound ConversationMessage",
|
|
526
|
+
{ cause: error }
|
|
527
|
+
);
|
|
528
|
+
}
|
|
479
529
|
const out = await handler(msg);
|
|
480
530
|
const reply = typeof out === "string" ? { content: out } : out;
|
|
481
531
|
return ConversationMessageSchema.parse({
|
|
@@ -494,8 +544,8 @@ var CeServiceAgent = class {
|
|
|
494
544
|
...reply.payload ? { payload: reply.payload } : {}
|
|
495
545
|
});
|
|
496
546
|
}
|
|
497
|
-
post(path, body) {
|
|
498
|
-
return this.fetchJson(path, { method: "POST", body: JSON.stringify(body) });
|
|
547
|
+
post(path, body, headers) {
|
|
548
|
+
return this.fetchJson(path, { method: "POST", body: JSON.stringify(body), ...headers ? { headers } : {} });
|
|
499
549
|
}
|
|
500
550
|
async fetchJson(path, init) {
|
|
501
551
|
const res = await this.fetchImpl(this.serverUrl + path, {
|
|
@@ -503,13 +553,240 @@ var CeServiceAgent = class {
|
|
|
503
553
|
headers: { "content-type": "application/json", ...init.headers ?? {} }
|
|
504
554
|
});
|
|
505
555
|
const text = await res.text();
|
|
506
|
-
const data = text ? JSON.parse(text) : void 0;
|
|
507
556
|
if (!res.ok) {
|
|
508
557
|
throw new Error(`server-ce ${path} -> ${res.status}: ${text}`);
|
|
509
558
|
}
|
|
510
|
-
return
|
|
559
|
+
if (!text) return void 0;
|
|
560
|
+
try {
|
|
561
|
+
return JSON.parse(text);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
throw new Error(`server-ce ${path} returned non-JSON success response: ${text}`, { cause: error });
|
|
564
|
+
}
|
|
511
565
|
}
|
|
512
566
|
};
|
|
567
|
+
|
|
568
|
+
// src/ce/serve.ts
|
|
569
|
+
import { createServer } from "http";
|
|
570
|
+
function send(res, status, body) {
|
|
571
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
572
|
+
res.end(JSON.stringify(body));
|
|
573
|
+
}
|
|
574
|
+
function readBody(req) {
|
|
575
|
+
return new Promise((resolve, reject) => {
|
|
576
|
+
let data = "";
|
|
577
|
+
req.on("data", (c) => data += c);
|
|
578
|
+
req.on("end", () => resolve(data));
|
|
579
|
+
req.on("error", reject);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function isLocalOnlyHost(host) {
|
|
583
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
584
|
+
}
|
|
585
|
+
function extractCallbackToken(req) {
|
|
586
|
+
const raw = req.headers["x-xtrape-service-callback-token"];
|
|
587
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
588
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
589
|
+
const auth = req.headers["authorization"];
|
|
590
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
591
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
592
|
+
return token.length > 0 ? token : void 0;
|
|
593
|
+
}
|
|
594
|
+
return void 0;
|
|
595
|
+
}
|
|
596
|
+
function requireCallbackAuthorized(req, res, callbackToken) {
|
|
597
|
+
if (!callbackToken || extractCallbackToken(req) === callbackToken) return true;
|
|
598
|
+
send(res, 401, {
|
|
599
|
+
code: "UNAUTHORIZED",
|
|
600
|
+
message: "service callback token is missing or invalid",
|
|
601
|
+
retryable: false
|
|
602
|
+
});
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function defaultLocalAddress(host, port) {
|
|
606
|
+
if (host === "::1") return `http://[::1]:${port}`;
|
|
607
|
+
return `http://${host}:${port}`;
|
|
608
|
+
}
|
|
609
|
+
function listen(server, port, host) {
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const onError = (error) => {
|
|
612
|
+
server.off("listening", onListening);
|
|
613
|
+
reject(error);
|
|
614
|
+
};
|
|
615
|
+
const onListening = () => {
|
|
616
|
+
server.off("error", onError);
|
|
617
|
+
resolve();
|
|
618
|
+
};
|
|
619
|
+
server.once("error", onError);
|
|
620
|
+
server.once("listening", onListening);
|
|
621
|
+
server.listen(port, host);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function sendMessageError(res, error) {
|
|
625
|
+
if (error instanceof CeInboundMessageValidationError) {
|
|
626
|
+
send(res, 400, { code: "VALIDATION_FAILED", message: error.message, retryable: false });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const message = error instanceof Error ? error.message : "message handler failed";
|
|
630
|
+
send(res, 500, { code: "HANDLER_FAILED", message, retryable: true });
|
|
631
|
+
}
|
|
632
|
+
function sendHandlerError(res, error) {
|
|
633
|
+
const message = error instanceof Error ? error.message : "action handler failed";
|
|
634
|
+
send(res, 500, { code: "ACTION_FAILED", message, retryable: false });
|
|
635
|
+
}
|
|
636
|
+
async function serveCeService(opts) {
|
|
637
|
+
const host = opts.host ?? "0.0.0.0";
|
|
638
|
+
let agent;
|
|
639
|
+
const server = createServer((req, res) => {
|
|
640
|
+
void (async () => {
|
|
641
|
+
try {
|
|
642
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
643
|
+
send(res, 200, { ok: true, service: opts.manifest.service.id });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (req.method === "POST" && req.url === "/messages") {
|
|
647
|
+
if (!agent) {
|
|
648
|
+
send(res, 503, { code: "NOT_READY", message: "service not registered yet", retryable: true });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
652
|
+
const raw = await readBody(req);
|
|
653
|
+
let payload;
|
|
654
|
+
try {
|
|
655
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
656
|
+
} catch (error) {
|
|
657
|
+
send(res, 400, {
|
|
658
|
+
code: "VALIDATION_FAILED",
|
|
659
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
660
|
+
retryable: false
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const reply = await agent.handleMessage(payload, opts.handler);
|
|
665
|
+
send(res, 200, reply);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (req.method === "GET" && req.url === "/actions") {
|
|
669
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
670
|
+
send(
|
|
671
|
+
res,
|
|
672
|
+
200,
|
|
673
|
+
(opts.actions ?? []).map((action) => ({
|
|
674
|
+
name: action.name,
|
|
675
|
+
label: action.label,
|
|
676
|
+
description: action.description,
|
|
677
|
+
category: action.category,
|
|
678
|
+
order: action.order,
|
|
679
|
+
requiresPrepare: typeof action.prepare === "function" ? true : void 0,
|
|
680
|
+
inputSchema: action.inputSchema
|
|
681
|
+
}))
|
|
682
|
+
);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const prepareMatch = req.url?.match(/^\/actions\/([^/]+)\/prepare$/);
|
|
686
|
+
if (req.method === "POST" && prepareMatch) {
|
|
687
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
688
|
+
const name = decodeURIComponent(prepareMatch[1]);
|
|
689
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
690
|
+
if (!action) {
|
|
691
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (typeof action.prepare !== "function") {
|
|
695
|
+
send(res, 400, { code: "ACTION_NOT_PREPARABLE", message: `action ${name} has no prepare step`, retryable: false });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const raw = await readBody(req);
|
|
699
|
+
let payload;
|
|
700
|
+
try {
|
|
701
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
702
|
+
} catch (error) {
|
|
703
|
+
send(res, 400, {
|
|
704
|
+
code: "VALIDATION_FAILED",
|
|
705
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
706
|
+
retryable: false
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
send(res, 200, await action.prepare(payload) ?? {});
|
|
712
|
+
} catch (error) {
|
|
713
|
+
sendHandlerError(res, error);
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const executeMatch = req.url?.match(/^\/actions\/([^/]+)\/execute$/);
|
|
718
|
+
if (req.method === "POST" && executeMatch) {
|
|
719
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
720
|
+
const name = decodeURIComponent(executeMatch[1]);
|
|
721
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
722
|
+
if (!action) {
|
|
723
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const raw = await readBody(req);
|
|
727
|
+
let payload;
|
|
728
|
+
try {
|
|
729
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
730
|
+
} catch (error) {
|
|
731
|
+
send(res, 400, {
|
|
732
|
+
code: "VALIDATION_FAILED",
|
|
733
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
734
|
+
retryable: false
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
send(res, 200, await action.execute(payload));
|
|
740
|
+
} catch (error) {
|
|
741
|
+
sendHandlerError(res, error);
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
send(res, 404, { code: "NOT_FOUND", message: "not found", retryable: false });
|
|
746
|
+
} catch (e) {
|
|
747
|
+
sendMessageError(res, e);
|
|
748
|
+
}
|
|
749
|
+
})();
|
|
750
|
+
});
|
|
751
|
+
await listen(server, opts.port ?? 0, host);
|
|
752
|
+
const info = server.address();
|
|
753
|
+
const port = typeof info === "object" && info ? info.port : opts.port ?? 0;
|
|
754
|
+
const address = opts.address ?? (isLocalOnlyHost(host) ? defaultLocalAddress(host, port) : void 0);
|
|
755
|
+
if (!address) {
|
|
756
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
757
|
+
throw new Error(
|
|
758
|
+
`serveCeService requires an explicit address when host=${host}. For deployed/containerized use, pass a reachable callback URL such as http://demo-service:8080.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
agent = new CeServiceAgent({
|
|
763
|
+
serverUrl: opts.serverUrl,
|
|
764
|
+
manifest: opts.manifest,
|
|
765
|
+
address,
|
|
766
|
+
instanceId: opts.instanceId,
|
|
767
|
+
registrationToken: opts.registrationToken,
|
|
768
|
+
controlToken: opts.controlToken,
|
|
769
|
+
fetchImpl: opts.fetchImpl
|
|
770
|
+
});
|
|
771
|
+
await agent.register();
|
|
772
|
+
if (opts.heartbeatMs !== 0) agent.startHeartbeat(opts.heartbeatMs ?? 1e4);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
agent?.stopHeartbeat();
|
|
775
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
agent,
|
|
780
|
+
address,
|
|
781
|
+
port,
|
|
782
|
+
async close() {
|
|
783
|
+
agent?.stopHeartbeat();
|
|
784
|
+
await agent?.deregister().catch(() => {
|
|
785
|
+
});
|
|
786
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
513
790
|
export {
|
|
514
791
|
AgentApiClient,
|
|
515
792
|
AgentApiError,
|
|
@@ -522,5 +799,6 @@ export {
|
|
|
522
799
|
FileTokenStore,
|
|
523
800
|
NetworkError,
|
|
524
801
|
RegistrationError,
|
|
525
|
-
CapsuleAgent as XtrapeAgent
|
|
802
|
+
CapsuleAgent as XtrapeAgent,
|
|
803
|
+
serveCeService
|
|
526
804
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtrape/capsule-agent-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Node.js Embedded Agent SDK for reporting Workers to the Xtrape control plane.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xtrape",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
],
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "git+https://
|
|
16
|
+
"url": "git+https://forgejo.xtrape.com/xtrape/xtrape-agent-nodejs.git"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://
|
|
18
|
+
"homepage": "https://forgejo.xtrape.com/xtrape/xtrape-agent-nodejs#readme",
|
|
19
19
|
"bugs": {
|
|
20
|
-
"url": "https://
|
|
20
|
+
"url": "https://forgejo.xtrape.com/xtrape/xtrape-agent-nodejs/issues"
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
23
|
"main": "./dist/index.cjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"lint": "tsc --noEmit"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@xtrape/capsule-contracts-node": "^0.
|
|
47
|
+
"@xtrape/capsule-contracts-node": "^0.6.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"tsup": "^8.3.5",
|