@xtrape/capsule-agent-node 0.6.0 → 0.6.2
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 +44 -2
- package/dist/index.cjs +176 -4
- package/dist/index.d.cts +91 -5
- package/dist/index.d.ts +91 -5
- package/dist/index.js +176 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,9 +48,11 @@ legacy `Capsule` concept.
|
|
|
48
48
|
|
|
49
49
|
### One-call bootstrap
|
|
50
50
|
|
|
51
|
-
`serveCeService` starts an HTTP listener (`POST /messages`,
|
|
51
|
+
`serveCeService` starts an HTTP listener (`POST /messages`, action endpoints,
|
|
52
|
+
`GET /health`),
|
|
52
53
|
registers the instance (sending the registration token if server-ce requires
|
|
53
|
-
one),
|
|
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:
|
|
54
56
|
|
|
55
57
|
For deployed or containerized use, set `address` to the callback URL that
|
|
56
58
|
`xtrape-server-ce` can actually reach and allowlist, for example
|
|
@@ -63,6 +65,13 @@ import { serveCeService } from "@xtrape/capsule-agent-node";
|
|
|
63
65
|
const svc = await serveCeService({
|
|
64
66
|
serverUrl: process.env.XTRAPE_SERVER_URL ?? "http://localhost:3000",
|
|
65
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,
|
|
66
75
|
port: 8080,
|
|
67
76
|
// required in deployed/containerized environments
|
|
68
77
|
address: process.env.SERVICE_ADDRESS ?? "http://demo-service:8080",
|
|
@@ -79,6 +88,38 @@ const svc = await serveCeService({
|
|
|
79
88
|
process.on("SIGTERM", () => void svc.close()); // stop heartbeat + deregister + close
|
|
80
89
|
```
|
|
81
90
|
|
|
91
|
+
### CE service actions
|
|
92
|
+
|
|
93
|
+
Pass `actions` to expose operator-triggerable operations over `GET /actions`,
|
|
94
|
+
`POST /actions/:name/prepare`, and `POST /actions/:name/execute`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const svc = await serveCeService({
|
|
98
|
+
// ...as above
|
|
99
|
+
actions: [
|
|
100
|
+
{
|
|
101
|
+
name: "relogin",
|
|
102
|
+
label: "Re-login account",
|
|
103
|
+
// Optional two-phase prepare: fetch live defaults/current state before
|
|
104
|
+
// the operator fills the form (advertised as requiresPrepare: true).
|
|
105
|
+
prepare: async () => ({ initialPayload: { accountId: "acc-1" } }),
|
|
106
|
+
execute: async (input) => ({ ok: true }),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "full-sync",
|
|
110
|
+
label: "Full re-sync",
|
|
111
|
+
// Long-running action (server-ce#41 Stage B): when the pushed execute
|
|
112
|
+
// carries x-xtrape-command-id, the SDK replies 202 RUNNING immediately,
|
|
113
|
+
// runs execute in the background, and reports the outcome to server-ce
|
|
114
|
+
// with the per-instance access token. Against an older server-ce
|
|
115
|
+
// (no header) the action falls back to synchronous execution.
|
|
116
|
+
async: true,
|
|
117
|
+
execute: async (input) => runFullSync(input),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
82
123
|
### Lower-level agent
|
|
83
124
|
|
|
84
125
|
When the service already runs its own HTTP server, use `CeServiceAgent` directly:
|
|
@@ -89,6 +130,7 @@ import { CeServiceAgent } from "@xtrape/capsule-agent-node";
|
|
|
89
130
|
const agent = new CeServiceAgent({
|
|
90
131
|
serverUrl: "http://localhost:3000",
|
|
91
132
|
registrationToken: process.env.SERVER_CE_REGISTRATION_TOKEN,
|
|
133
|
+
controlToken: process.env.SERVER_CE_CONTROL_TOKEN, // compatibility fallback only
|
|
92
134
|
address: "http://127.0.0.1:8080",
|
|
93
135
|
manifest: /* ServiceManifest */ undefined as never,
|
|
94
136
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -446,7 +446,9 @@ var CeServiceAgent = class {
|
|
|
446
446
|
address;
|
|
447
447
|
tenant;
|
|
448
448
|
registrationToken;
|
|
449
|
+
controlToken;
|
|
449
450
|
fetchImpl;
|
|
451
|
+
accessToken;
|
|
450
452
|
health = "HEALTHY";
|
|
451
453
|
hbTimer;
|
|
452
454
|
constructor(opts) {
|
|
@@ -457,8 +459,18 @@ var CeServiceAgent = class {
|
|
|
457
459
|
this.serviceId = opts.manifest.service.id;
|
|
458
460
|
this.instanceId = opts.instanceId ?? `${this.serviceId}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
|
459
461
|
this.registrationToken = opts.registrationToken;
|
|
462
|
+
this.controlToken = opts.controlToken;
|
|
460
463
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
461
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
|
+
}
|
|
462
474
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
463
475
|
instance() {
|
|
464
476
|
return import_capsule_contracts_node.ServiceInstanceSchema.parse({
|
|
@@ -474,8 +486,12 @@ var CeServiceAgent = class {
|
|
|
474
486
|
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
475
487
|
async register() {
|
|
476
488
|
const headers = this.registrationToken ? { "x-xtrape-registration-token": this.registrationToken } : void 0;
|
|
477
|
-
await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() }, headers);
|
|
478
|
-
|
|
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 } : {} };
|
|
479
495
|
}
|
|
480
496
|
/**
|
|
481
497
|
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
@@ -505,7 +521,7 @@ var CeServiceAgent = class {
|
|
|
505
521
|
health: this.health,
|
|
506
522
|
...metricsSummary ? { metricsSummary } : {}
|
|
507
523
|
};
|
|
508
|
-
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
|
|
524
|
+
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb, this.lifecycleAuthHeaders());
|
|
509
525
|
}
|
|
510
526
|
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
511
527
|
startHeartbeat(intervalMs = 1e4, metrics) {
|
|
@@ -525,7 +541,31 @@ var CeServiceAgent = class {
|
|
|
525
541
|
}
|
|
526
542
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
527
543
|
async deregister() {
|
|
528
|
-
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
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Report the outcome of an async action (server-ce#41 Stage B):
|
|
551
|
+
* POST /v1/instances/{instanceId}/commands/{commandId}/result using the
|
|
552
|
+
* per-instance access token. `outcome.result` carries the action payload for
|
|
553
|
+
* COMPLETED; errorCode/errorMessage describe a FAILED run.
|
|
554
|
+
*/
|
|
555
|
+
async reportCommandResult(commandId, outcome) {
|
|
556
|
+
const body = {
|
|
557
|
+
contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
|
|
558
|
+
commandId,
|
|
559
|
+
status: outcome.status,
|
|
560
|
+
...outcome.result !== void 0 ? { result: outcome.result } : {},
|
|
561
|
+
...outcome.errorCode ? { errorCode: outcome.errorCode } : {},
|
|
562
|
+
...outcome.errorMessage ? { errorMessage: outcome.errorMessage } : {}
|
|
563
|
+
};
|
|
564
|
+
await this.post(
|
|
565
|
+
`/v1/instances/${encodeURIComponent(this.instanceId)}/commands/${encodeURIComponent(commandId)}/result`,
|
|
566
|
+
body,
|
|
567
|
+
this.lifecycleAuthHeaders()
|
|
568
|
+
);
|
|
529
569
|
}
|
|
530
570
|
/**
|
|
531
571
|
* Validate an inbound message, run the handler, and build the reply message
|
|
@@ -582,6 +622,7 @@ var CeServiceAgent = class {
|
|
|
582
622
|
|
|
583
623
|
// src/ce/serve.ts
|
|
584
624
|
var import_node_http = require("http");
|
|
625
|
+
var import_capsule_contracts_node2 = require("@xtrape/capsule-contracts-node");
|
|
585
626
|
function send(res, status, body) {
|
|
586
627
|
res.writeHead(status, { "content-type": "application/json" });
|
|
587
628
|
res.end(JSON.stringify(body));
|
|
@@ -597,6 +638,26 @@ function readBody(req) {
|
|
|
597
638
|
function isLocalOnlyHost(host) {
|
|
598
639
|
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
599
640
|
}
|
|
641
|
+
function extractCallbackToken(req) {
|
|
642
|
+
const raw = req.headers["x-xtrape-service-callback-token"];
|
|
643
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
644
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
645
|
+
const auth = req.headers["authorization"];
|
|
646
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
647
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
648
|
+
return token.length > 0 ? token : void 0;
|
|
649
|
+
}
|
|
650
|
+
return void 0;
|
|
651
|
+
}
|
|
652
|
+
function requireCallbackAuthorized(req, res, callbackToken) {
|
|
653
|
+
if (!callbackToken || extractCallbackToken(req) === callbackToken) return true;
|
|
654
|
+
send(res, 401, {
|
|
655
|
+
code: "UNAUTHORIZED",
|
|
656
|
+
message: "service callback token is missing or invalid",
|
|
657
|
+
retryable: false
|
|
658
|
+
});
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
600
661
|
function defaultLocalAddress(host, port) {
|
|
601
662
|
if (host === "::1") return `http://[::1]:${port}`;
|
|
602
663
|
return `http://${host}:${port}`;
|
|
@@ -624,6 +685,10 @@ function sendMessageError(res, error) {
|
|
|
624
685
|
const message = error instanceof Error ? error.message : "message handler failed";
|
|
625
686
|
send(res, 500, { code: "HANDLER_FAILED", message, retryable: true });
|
|
626
687
|
}
|
|
688
|
+
function sendHandlerError(res, error) {
|
|
689
|
+
const message = error instanceof Error ? error.message : "action handler failed";
|
|
690
|
+
send(res, 500, { code: "ACTION_FAILED", message, retryable: false });
|
|
691
|
+
}
|
|
627
692
|
async function serveCeService(opts) {
|
|
628
693
|
const host = opts.host ?? "0.0.0.0";
|
|
629
694
|
let agent;
|
|
@@ -639,6 +704,7 @@ async function serveCeService(opts) {
|
|
|
639
704
|
send(res, 503, { code: "NOT_READY", message: "service not registered yet", retryable: true });
|
|
640
705
|
return;
|
|
641
706
|
}
|
|
707
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
642
708
|
const raw = await readBody(req);
|
|
643
709
|
let payload;
|
|
644
710
|
try {
|
|
@@ -655,6 +721,111 @@ async function serveCeService(opts) {
|
|
|
655
721
|
send(res, 200, reply);
|
|
656
722
|
return;
|
|
657
723
|
}
|
|
724
|
+
if (req.method === "GET" && req.url === "/actions") {
|
|
725
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
726
|
+
send(
|
|
727
|
+
res,
|
|
728
|
+
200,
|
|
729
|
+
(opts.actions ?? []).map((action) => ({
|
|
730
|
+
name: action.name,
|
|
731
|
+
label: action.label,
|
|
732
|
+
description: action.description,
|
|
733
|
+
category: action.category,
|
|
734
|
+
order: action.order,
|
|
735
|
+
requiresPrepare: typeof action.prepare === "function" ? true : void 0,
|
|
736
|
+
async: action.async === true ? true : void 0,
|
|
737
|
+
inputSchema: action.inputSchema
|
|
738
|
+
}))
|
|
739
|
+
);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const prepareMatch = req.url?.match(/^\/actions\/([^/]+)\/prepare$/);
|
|
743
|
+
if (req.method === "POST" && prepareMatch) {
|
|
744
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
745
|
+
const name = decodeURIComponent(prepareMatch[1]);
|
|
746
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
747
|
+
if (!action) {
|
|
748
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (typeof action.prepare !== "function") {
|
|
752
|
+
send(res, 400, { code: "ACTION_NOT_PREPARABLE", message: `action ${name} has no prepare step`, retryable: false });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const raw = await readBody(req);
|
|
756
|
+
let payload;
|
|
757
|
+
try {
|
|
758
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
759
|
+
} catch (error) {
|
|
760
|
+
send(res, 400, {
|
|
761
|
+
code: "VALIDATION_FAILED",
|
|
762
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
763
|
+
retryable: false
|
|
764
|
+
});
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
send(res, 200, await action.prepare(payload) ?? {});
|
|
769
|
+
} catch (error) {
|
|
770
|
+
sendHandlerError(res, error);
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const executeMatch = req.url?.match(/^\/actions\/([^/]+)\/execute$/);
|
|
775
|
+
if (req.method === "POST" && executeMatch) {
|
|
776
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
777
|
+
const name = decodeURIComponent(executeMatch[1]);
|
|
778
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
779
|
+
if (!action) {
|
|
780
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const raw = await readBody(req);
|
|
784
|
+
let payload;
|
|
785
|
+
try {
|
|
786
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
787
|
+
} catch (error) {
|
|
788
|
+
send(res, 400, {
|
|
789
|
+
code: "VALIDATION_FAILED",
|
|
790
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
791
|
+
retryable: false
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const commandIdRaw = req.headers["x-xtrape-command-id"];
|
|
796
|
+
const commandId = Array.isArray(commandIdRaw) ? commandIdRaw[0] : commandIdRaw;
|
|
797
|
+
if (action.async === true && typeof commandId === "string" && commandId.length > 0 && agent) {
|
|
798
|
+
const reporter = agent;
|
|
799
|
+
send(res, 202, { contractVersion: import_capsule_contracts_node2.CE_CONTRACT_VERSION, status: "RUNNING", commandId });
|
|
800
|
+
void (async () => {
|
|
801
|
+
let outcome;
|
|
802
|
+
try {
|
|
803
|
+
const result = await action.execute(payload);
|
|
804
|
+
outcome = {
|
|
805
|
+
status: "COMPLETED",
|
|
806
|
+
...result !== void 0 && result !== null && typeof result === "object" && !Array.isArray(result) ? { result } : result !== void 0 ? { result: { value: result } } : {}
|
|
807
|
+
};
|
|
808
|
+
} catch (error) {
|
|
809
|
+
outcome = {
|
|
810
|
+
status: "FAILED",
|
|
811
|
+
errorCode: "ACTION_FAILED",
|
|
812
|
+
errorMessage: error instanceof Error ? error.message : "action handler failed"
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
await reporter.reportCommandResult(commandId, outcome);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
})();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
send(res, 200, await action.execute(payload));
|
|
824
|
+
} catch (error) {
|
|
825
|
+
sendHandlerError(res, error);
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
658
829
|
send(res, 404, { code: "NOT_FOUND", message: "not found", retryable: false });
|
|
659
830
|
} catch (e) {
|
|
660
831
|
sendMessageError(res, e);
|
|
@@ -678,6 +849,7 @@ async function serveCeService(opts) {
|
|
|
678
849
|
address,
|
|
679
850
|
instanceId: opts.instanceId,
|
|
680
851
|
registrationToken: opts.registrationToken,
|
|
852
|
+
controlToken: opts.controlToken,
|
|
681
853
|
fetchImpl: opts.fetchImpl
|
|
682
854
|
});
|
|
683
855
|
await agent.register();
|
package/dist/index.d.cts
CHANGED
|
@@ -283,9 +283,21 @@ interface CeServiceAgentOptions {
|
|
|
283
283
|
* `x-xtrape-registration-token` header on register.
|
|
284
284
|
*/
|
|
285
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;
|
|
286
294
|
/** injectable fetch (tests); defaults to global fetch */
|
|
287
295
|
fetchImpl?: typeof fetch;
|
|
288
296
|
}
|
|
297
|
+
interface CeServiceRegistrationResult {
|
|
298
|
+
instanceId: string;
|
|
299
|
+
accessToken?: string;
|
|
300
|
+
}
|
|
289
301
|
type CeMessageReply = string | {
|
|
290
302
|
content: string;
|
|
291
303
|
messageType?: ConversationMessage["messageType"];
|
|
@@ -301,16 +313,22 @@ declare class CeServiceAgent {
|
|
|
301
313
|
private readonly address;
|
|
302
314
|
private readonly tenant;
|
|
303
315
|
private readonly registrationToken?;
|
|
316
|
+
private readonly controlToken?;
|
|
304
317
|
private readonly fetchImpl;
|
|
318
|
+
private accessToken?;
|
|
305
319
|
private health;
|
|
306
320
|
private hbTimer?;
|
|
307
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;
|
|
308
328
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
309
329
|
instance(): ServiceInstance;
|
|
310
330
|
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
311
|
-
register(): Promise<
|
|
312
|
-
instanceId: string;
|
|
313
|
-
}>;
|
|
331
|
+
register(): Promise<CeServiceRegistrationResult>;
|
|
314
332
|
/**
|
|
315
333
|
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
316
334
|
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
@@ -328,6 +346,18 @@ declare class CeServiceAgent {
|
|
|
328
346
|
stopHeartbeat(): void;
|
|
329
347
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
330
348
|
deregister(): Promise<void>;
|
|
349
|
+
/**
|
|
350
|
+
* Report the outcome of an async action (server-ce#41 Stage B):
|
|
351
|
+
* POST /v1/instances/{instanceId}/commands/{commandId}/result using the
|
|
352
|
+
* per-instance access token. `outcome.result` carries the action payload for
|
|
353
|
+
* COMPLETED; errorCode/errorMessage describe a FAILED run.
|
|
354
|
+
*/
|
|
355
|
+
reportCommandResult(commandId: string, outcome: {
|
|
356
|
+
status: "COMPLETED" | "FAILED";
|
|
357
|
+
result?: Record<string, unknown>;
|
|
358
|
+
errorCode?: string;
|
|
359
|
+
errorMessage?: string;
|
|
360
|
+
}): Promise<void>;
|
|
331
361
|
/**
|
|
332
362
|
* Validate an inbound message, run the handler, and build the reply message
|
|
333
363
|
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
@@ -339,7 +369,7 @@ declare class CeServiceAgent {
|
|
|
339
369
|
|
|
340
370
|
/**
|
|
341
371
|
* One-call bootstrap for an `xtrape-service`: start an HTTP listener that serves
|
|
342
|
-
* `POST /messages
|
|
372
|
+
* `POST /messages`, action endpoints (and `GET /health`), register the instance to xtrape-server-ce,
|
|
343
373
|
* and run heartbeats. Returns a handle whose `close()` stops heartbeats,
|
|
344
374
|
* deregisters, and closes the listener.
|
|
345
375
|
*
|
|
@@ -352,8 +382,25 @@ interface ServeCeServiceOptions {
|
|
|
352
382
|
manifest: ServiceManifest;
|
|
353
383
|
/** business handler invoked per inbound ConversationMessage */
|
|
354
384
|
handler: CeMessageHandler;
|
|
385
|
+
/** Optional CE action handlers exposed over GET /actions, POST /actions/:name/prepare, and POST /actions/:name/execute. */
|
|
386
|
+
actions?: CeServiceActionDefinition[];
|
|
355
387
|
/** shared registration token, if server-ce requires one */
|
|
356
388
|
registrationToken?: string;
|
|
389
|
+
/**
|
|
390
|
+
* 0.6.x control-plane token forwarded to server-ce on heartbeat/deregister
|
|
391
|
+
* only as a compatibility fallback. Current server-ce returns a per-instance
|
|
392
|
+
* access token during registration and the SDK uses that first.
|
|
393
|
+
*/
|
|
394
|
+
controlToken?: string;
|
|
395
|
+
/**
|
|
396
|
+
* Optional inbound callback token. When set, `POST /messages` and action
|
|
397
|
+
* endpoints (`GET /actions`, `POST /actions/:name/prepare`,
|
|
398
|
+
* `POST /actions/:name/execute`) must include
|
|
399
|
+
* `x-xtrape-service-callback-token: <callbackToken>` (or
|
|
400
|
+
* `Authorization: Bearer <callbackToken>`); otherwise the request is rejected
|
|
401
|
+
* with 401. Lets a service require authenticated callbacks from server-ce.
|
|
402
|
+
*/
|
|
403
|
+
callbackToken?: string;
|
|
357
404
|
/** listen port; 0 picks an ephemeral port (default 0) */
|
|
358
405
|
port?: number;
|
|
359
406
|
/** listen host (default 0.0.0.0) */
|
|
@@ -372,6 +419,45 @@ interface ServeCeServiceOptions {
|
|
|
372
419
|
/** injectable fetch (tests); defaults to global fetch */
|
|
373
420
|
fetchImpl?: typeof fetch;
|
|
374
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* What a two-phase action's prepare step returns to the operator UI:
|
|
424
|
+
* `initialPayload` seeds the execute form, `currentState` is contextual
|
|
425
|
+
* data to display (e.g. the accounts/balances the decision is based on),
|
|
426
|
+
* and `inputSchema` optionally overrides the static schema with live options.
|
|
427
|
+
*/
|
|
428
|
+
interface CeActionPrepareResult {
|
|
429
|
+
/** Longer operator-facing explanation shown on the execution screen. */
|
|
430
|
+
longDescription?: string;
|
|
431
|
+
initialPayload?: Record<string, unknown>;
|
|
432
|
+
currentState?: Record<string, unknown>;
|
|
433
|
+
inputSchema?: Record<string, unknown>;
|
|
434
|
+
}
|
|
435
|
+
interface CeServiceActionDefinition {
|
|
436
|
+
name: string;
|
|
437
|
+
label: string;
|
|
438
|
+
description?: string;
|
|
439
|
+
/** Operator-facing grouping shown by Panel (capsule ActionDefinition heritage), e.g. "diagnostics". */
|
|
440
|
+
category?: string;
|
|
441
|
+
/** Display order within the category; lower sorts first. */
|
|
442
|
+
order?: number;
|
|
443
|
+
inputSchema?: Record<string, unknown>;
|
|
444
|
+
/**
|
|
445
|
+
* Optional prepare step. When present the action is two-phase: server-ce
|
|
446
|
+
* calls this before the operator fills the form to fetch live defaults and
|
|
447
|
+
* current state. Advertised as `requiresPrepare: true` in the action list.
|
|
448
|
+
*/
|
|
449
|
+
prepare?(input?: unknown): CeActionPrepareResult | Promise<CeActionPrepareResult>;
|
|
450
|
+
/**
|
|
451
|
+
* Long-running action (server-ce#41 Stage B). When true and the pushed
|
|
452
|
+
* execute carries an `x-xtrape-command-id` header, the listener replies
|
|
453
|
+
* 202 { status: "RUNNING", commandId } immediately, runs `execute` in the
|
|
454
|
+
* background, and reports the outcome via
|
|
455
|
+
* `CeServiceAgent.reportCommandResult`. Without the header (older server-ce)
|
|
456
|
+
* the action falls back to synchronous execution.
|
|
457
|
+
*/
|
|
458
|
+
async?: boolean;
|
|
459
|
+
execute(input: unknown): unknown | Promise<unknown>;
|
|
460
|
+
}
|
|
375
461
|
interface ServeCeServiceHandle {
|
|
376
462
|
agent: CeServiceAgent;
|
|
377
463
|
/** advertised address registered with server-ce */
|
|
@@ -383,4 +469,4 @@ interface ServeCeServiceHandle {
|
|
|
383
469
|
}
|
|
384
470
|
declare function serveCeService(opts: ServeCeServiceOptions): Promise<ServeCeServiceHandle>;
|
|
385
471
|
|
|
386
|
-
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type CeMessageHandler, type CeMessageReply, 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 };
|
|
472
|
+
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
|
@@ -283,9 +283,21 @@ interface CeServiceAgentOptions {
|
|
|
283
283
|
* `x-xtrape-registration-token` header on register.
|
|
284
284
|
*/
|
|
285
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;
|
|
286
294
|
/** injectable fetch (tests); defaults to global fetch */
|
|
287
295
|
fetchImpl?: typeof fetch;
|
|
288
296
|
}
|
|
297
|
+
interface CeServiceRegistrationResult {
|
|
298
|
+
instanceId: string;
|
|
299
|
+
accessToken?: string;
|
|
300
|
+
}
|
|
289
301
|
type CeMessageReply = string | {
|
|
290
302
|
content: string;
|
|
291
303
|
messageType?: ConversationMessage["messageType"];
|
|
@@ -301,16 +313,22 @@ declare class CeServiceAgent {
|
|
|
301
313
|
private readonly address;
|
|
302
314
|
private readonly tenant;
|
|
303
315
|
private readonly registrationToken?;
|
|
316
|
+
private readonly controlToken?;
|
|
304
317
|
private readonly fetchImpl;
|
|
318
|
+
private accessToken?;
|
|
305
319
|
private health;
|
|
306
320
|
private hbTimer?;
|
|
307
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;
|
|
308
328
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
309
329
|
instance(): ServiceInstance;
|
|
310
330
|
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
311
|
-
register(): Promise<
|
|
312
|
-
instanceId: string;
|
|
313
|
-
}>;
|
|
331
|
+
register(): Promise<CeServiceRegistrationResult>;
|
|
314
332
|
/**
|
|
315
333
|
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
316
334
|
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
@@ -328,6 +346,18 @@ declare class CeServiceAgent {
|
|
|
328
346
|
stopHeartbeat(): void;
|
|
329
347
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
330
348
|
deregister(): Promise<void>;
|
|
349
|
+
/**
|
|
350
|
+
* Report the outcome of an async action (server-ce#41 Stage B):
|
|
351
|
+
* POST /v1/instances/{instanceId}/commands/{commandId}/result using the
|
|
352
|
+
* per-instance access token. `outcome.result` carries the action payload for
|
|
353
|
+
* COMPLETED; errorCode/errorMessage describe a FAILED run.
|
|
354
|
+
*/
|
|
355
|
+
reportCommandResult(commandId: string, outcome: {
|
|
356
|
+
status: "COMPLETED" | "FAILED";
|
|
357
|
+
result?: Record<string, unknown>;
|
|
358
|
+
errorCode?: string;
|
|
359
|
+
errorMessage?: string;
|
|
360
|
+
}): Promise<void>;
|
|
331
361
|
/**
|
|
332
362
|
* Validate an inbound message, run the handler, and build the reply message
|
|
333
363
|
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
@@ -339,7 +369,7 @@ declare class CeServiceAgent {
|
|
|
339
369
|
|
|
340
370
|
/**
|
|
341
371
|
* One-call bootstrap for an `xtrape-service`: start an HTTP listener that serves
|
|
342
|
-
* `POST /messages
|
|
372
|
+
* `POST /messages`, action endpoints (and `GET /health`), register the instance to xtrape-server-ce,
|
|
343
373
|
* and run heartbeats. Returns a handle whose `close()` stops heartbeats,
|
|
344
374
|
* deregisters, and closes the listener.
|
|
345
375
|
*
|
|
@@ -352,8 +382,25 @@ interface ServeCeServiceOptions {
|
|
|
352
382
|
manifest: ServiceManifest;
|
|
353
383
|
/** business handler invoked per inbound ConversationMessage */
|
|
354
384
|
handler: CeMessageHandler;
|
|
385
|
+
/** Optional CE action handlers exposed over GET /actions, POST /actions/:name/prepare, and POST /actions/:name/execute. */
|
|
386
|
+
actions?: CeServiceActionDefinition[];
|
|
355
387
|
/** shared registration token, if server-ce requires one */
|
|
356
388
|
registrationToken?: string;
|
|
389
|
+
/**
|
|
390
|
+
* 0.6.x control-plane token forwarded to server-ce on heartbeat/deregister
|
|
391
|
+
* only as a compatibility fallback. Current server-ce returns a per-instance
|
|
392
|
+
* access token during registration and the SDK uses that first.
|
|
393
|
+
*/
|
|
394
|
+
controlToken?: string;
|
|
395
|
+
/**
|
|
396
|
+
* Optional inbound callback token. When set, `POST /messages` and action
|
|
397
|
+
* endpoints (`GET /actions`, `POST /actions/:name/prepare`,
|
|
398
|
+
* `POST /actions/:name/execute`) must include
|
|
399
|
+
* `x-xtrape-service-callback-token: <callbackToken>` (or
|
|
400
|
+
* `Authorization: Bearer <callbackToken>`); otherwise the request is rejected
|
|
401
|
+
* with 401. Lets a service require authenticated callbacks from server-ce.
|
|
402
|
+
*/
|
|
403
|
+
callbackToken?: string;
|
|
357
404
|
/** listen port; 0 picks an ephemeral port (default 0) */
|
|
358
405
|
port?: number;
|
|
359
406
|
/** listen host (default 0.0.0.0) */
|
|
@@ -372,6 +419,45 @@ interface ServeCeServiceOptions {
|
|
|
372
419
|
/** injectable fetch (tests); defaults to global fetch */
|
|
373
420
|
fetchImpl?: typeof fetch;
|
|
374
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* What a two-phase action's prepare step returns to the operator UI:
|
|
424
|
+
* `initialPayload` seeds the execute form, `currentState` is contextual
|
|
425
|
+
* data to display (e.g. the accounts/balances the decision is based on),
|
|
426
|
+
* and `inputSchema` optionally overrides the static schema with live options.
|
|
427
|
+
*/
|
|
428
|
+
interface CeActionPrepareResult {
|
|
429
|
+
/** Longer operator-facing explanation shown on the execution screen. */
|
|
430
|
+
longDescription?: string;
|
|
431
|
+
initialPayload?: Record<string, unknown>;
|
|
432
|
+
currentState?: Record<string, unknown>;
|
|
433
|
+
inputSchema?: Record<string, unknown>;
|
|
434
|
+
}
|
|
435
|
+
interface CeServiceActionDefinition {
|
|
436
|
+
name: string;
|
|
437
|
+
label: string;
|
|
438
|
+
description?: string;
|
|
439
|
+
/** Operator-facing grouping shown by Panel (capsule ActionDefinition heritage), e.g. "diagnostics". */
|
|
440
|
+
category?: string;
|
|
441
|
+
/** Display order within the category; lower sorts first. */
|
|
442
|
+
order?: number;
|
|
443
|
+
inputSchema?: Record<string, unknown>;
|
|
444
|
+
/**
|
|
445
|
+
* Optional prepare step. When present the action is two-phase: server-ce
|
|
446
|
+
* calls this before the operator fills the form to fetch live defaults and
|
|
447
|
+
* current state. Advertised as `requiresPrepare: true` in the action list.
|
|
448
|
+
*/
|
|
449
|
+
prepare?(input?: unknown): CeActionPrepareResult | Promise<CeActionPrepareResult>;
|
|
450
|
+
/**
|
|
451
|
+
* Long-running action (server-ce#41 Stage B). When true and the pushed
|
|
452
|
+
* execute carries an `x-xtrape-command-id` header, the listener replies
|
|
453
|
+
* 202 { status: "RUNNING", commandId } immediately, runs `execute` in the
|
|
454
|
+
* background, and reports the outcome via
|
|
455
|
+
* `CeServiceAgent.reportCommandResult`. Without the header (older server-ce)
|
|
456
|
+
* the action falls back to synchronous execution.
|
|
457
|
+
*/
|
|
458
|
+
async?: boolean;
|
|
459
|
+
execute(input: unknown): unknown | Promise<unknown>;
|
|
460
|
+
}
|
|
375
461
|
interface ServeCeServiceHandle {
|
|
376
462
|
agent: CeServiceAgent;
|
|
377
463
|
/** advertised address registered with server-ce */
|
|
@@ -383,4 +469,4 @@ interface ServeCeServiceHandle {
|
|
|
383
469
|
}
|
|
384
470
|
declare function serveCeService(opts: ServeCeServiceOptions): Promise<ServeCeServiceHandle>;
|
|
385
471
|
|
|
386
|
-
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type CeMessageHandler, type CeMessageReply, 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 };
|
|
472
|
+
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
|
@@ -412,7 +412,9 @@ var CeServiceAgent = class {
|
|
|
412
412
|
address;
|
|
413
413
|
tenant;
|
|
414
414
|
registrationToken;
|
|
415
|
+
controlToken;
|
|
415
416
|
fetchImpl;
|
|
417
|
+
accessToken;
|
|
416
418
|
health = "HEALTHY";
|
|
417
419
|
hbTimer;
|
|
418
420
|
constructor(opts) {
|
|
@@ -423,8 +425,18 @@ var CeServiceAgent = class {
|
|
|
423
425
|
this.serviceId = opts.manifest.service.id;
|
|
424
426
|
this.instanceId = opts.instanceId ?? `${this.serviceId}-${randomUUID().slice(0, 8)}`;
|
|
425
427
|
this.registrationToken = opts.registrationToken;
|
|
428
|
+
this.controlToken = opts.controlToken;
|
|
426
429
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
427
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
|
+
}
|
|
428
440
|
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
429
441
|
instance() {
|
|
430
442
|
return ServiceInstanceSchema.parse({
|
|
@@ -440,8 +452,12 @@ var CeServiceAgent = class {
|
|
|
440
452
|
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
441
453
|
async register() {
|
|
442
454
|
const headers = this.registrationToken ? { "x-xtrape-registration-token": this.registrationToken } : void 0;
|
|
443
|
-
await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() }, headers);
|
|
444
|
-
|
|
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 } : {} };
|
|
445
461
|
}
|
|
446
462
|
/**
|
|
447
463
|
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
@@ -471,7 +487,7 @@ var CeServiceAgent = class {
|
|
|
471
487
|
health: this.health,
|
|
472
488
|
...metricsSummary ? { metricsSummary } : {}
|
|
473
489
|
};
|
|
474
|
-
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
|
|
490
|
+
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb, this.lifecycleAuthHeaders());
|
|
475
491
|
}
|
|
476
492
|
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
477
493
|
startHeartbeat(intervalMs = 1e4, metrics) {
|
|
@@ -491,7 +507,31 @@ var CeServiceAgent = class {
|
|
|
491
507
|
}
|
|
492
508
|
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
493
509
|
async deregister() {
|
|
494
|
-
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
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Report the outcome of an async action (server-ce#41 Stage B):
|
|
517
|
+
* POST /v1/instances/{instanceId}/commands/{commandId}/result using the
|
|
518
|
+
* per-instance access token. `outcome.result` carries the action payload for
|
|
519
|
+
* COMPLETED; errorCode/errorMessage describe a FAILED run.
|
|
520
|
+
*/
|
|
521
|
+
async reportCommandResult(commandId, outcome) {
|
|
522
|
+
const body = {
|
|
523
|
+
contractVersion: CE_CONTRACT_VERSION,
|
|
524
|
+
commandId,
|
|
525
|
+
status: outcome.status,
|
|
526
|
+
...outcome.result !== void 0 ? { result: outcome.result } : {},
|
|
527
|
+
...outcome.errorCode ? { errorCode: outcome.errorCode } : {},
|
|
528
|
+
...outcome.errorMessage ? { errorMessage: outcome.errorMessage } : {}
|
|
529
|
+
};
|
|
530
|
+
await this.post(
|
|
531
|
+
`/v1/instances/${encodeURIComponent(this.instanceId)}/commands/${encodeURIComponent(commandId)}/result`,
|
|
532
|
+
body,
|
|
533
|
+
this.lifecycleAuthHeaders()
|
|
534
|
+
);
|
|
495
535
|
}
|
|
496
536
|
/**
|
|
497
537
|
* Validate an inbound message, run the handler, and build the reply message
|
|
@@ -548,6 +588,7 @@ var CeServiceAgent = class {
|
|
|
548
588
|
|
|
549
589
|
// src/ce/serve.ts
|
|
550
590
|
import { createServer } from "http";
|
|
591
|
+
import { CE_CONTRACT_VERSION as CE_CONTRACT_VERSION2 } from "@xtrape/capsule-contracts-node";
|
|
551
592
|
function send(res, status, body) {
|
|
552
593
|
res.writeHead(status, { "content-type": "application/json" });
|
|
553
594
|
res.end(JSON.stringify(body));
|
|
@@ -563,6 +604,26 @@ function readBody(req) {
|
|
|
563
604
|
function isLocalOnlyHost(host) {
|
|
564
605
|
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
565
606
|
}
|
|
607
|
+
function extractCallbackToken(req) {
|
|
608
|
+
const raw = req.headers["x-xtrape-service-callback-token"];
|
|
609
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
610
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
611
|
+
const auth = req.headers["authorization"];
|
|
612
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
613
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
614
|
+
return token.length > 0 ? token : void 0;
|
|
615
|
+
}
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
function requireCallbackAuthorized(req, res, callbackToken) {
|
|
619
|
+
if (!callbackToken || extractCallbackToken(req) === callbackToken) return true;
|
|
620
|
+
send(res, 401, {
|
|
621
|
+
code: "UNAUTHORIZED",
|
|
622
|
+
message: "service callback token is missing or invalid",
|
|
623
|
+
retryable: false
|
|
624
|
+
});
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
566
627
|
function defaultLocalAddress(host, port) {
|
|
567
628
|
if (host === "::1") return `http://[::1]:${port}`;
|
|
568
629
|
return `http://${host}:${port}`;
|
|
@@ -590,6 +651,10 @@ function sendMessageError(res, error) {
|
|
|
590
651
|
const message = error instanceof Error ? error.message : "message handler failed";
|
|
591
652
|
send(res, 500, { code: "HANDLER_FAILED", message, retryable: true });
|
|
592
653
|
}
|
|
654
|
+
function sendHandlerError(res, error) {
|
|
655
|
+
const message = error instanceof Error ? error.message : "action handler failed";
|
|
656
|
+
send(res, 500, { code: "ACTION_FAILED", message, retryable: false });
|
|
657
|
+
}
|
|
593
658
|
async function serveCeService(opts) {
|
|
594
659
|
const host = opts.host ?? "0.0.0.0";
|
|
595
660
|
let agent;
|
|
@@ -605,6 +670,7 @@ async function serveCeService(opts) {
|
|
|
605
670
|
send(res, 503, { code: "NOT_READY", message: "service not registered yet", retryable: true });
|
|
606
671
|
return;
|
|
607
672
|
}
|
|
673
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
608
674
|
const raw = await readBody(req);
|
|
609
675
|
let payload;
|
|
610
676
|
try {
|
|
@@ -621,6 +687,111 @@ async function serveCeService(opts) {
|
|
|
621
687
|
send(res, 200, reply);
|
|
622
688
|
return;
|
|
623
689
|
}
|
|
690
|
+
if (req.method === "GET" && req.url === "/actions") {
|
|
691
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
692
|
+
send(
|
|
693
|
+
res,
|
|
694
|
+
200,
|
|
695
|
+
(opts.actions ?? []).map((action) => ({
|
|
696
|
+
name: action.name,
|
|
697
|
+
label: action.label,
|
|
698
|
+
description: action.description,
|
|
699
|
+
category: action.category,
|
|
700
|
+
order: action.order,
|
|
701
|
+
requiresPrepare: typeof action.prepare === "function" ? true : void 0,
|
|
702
|
+
async: action.async === true ? true : void 0,
|
|
703
|
+
inputSchema: action.inputSchema
|
|
704
|
+
}))
|
|
705
|
+
);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const prepareMatch = req.url?.match(/^\/actions\/([^/]+)\/prepare$/);
|
|
709
|
+
if (req.method === "POST" && prepareMatch) {
|
|
710
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
711
|
+
const name = decodeURIComponent(prepareMatch[1]);
|
|
712
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
713
|
+
if (!action) {
|
|
714
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (typeof action.prepare !== "function") {
|
|
718
|
+
send(res, 400, { code: "ACTION_NOT_PREPARABLE", message: `action ${name} has no prepare step`, retryable: false });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const raw = await readBody(req);
|
|
722
|
+
let payload;
|
|
723
|
+
try {
|
|
724
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
725
|
+
} catch (error) {
|
|
726
|
+
send(res, 400, {
|
|
727
|
+
code: "VALIDATION_FAILED",
|
|
728
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
729
|
+
retryable: false
|
|
730
|
+
});
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
send(res, 200, await action.prepare(payload) ?? {});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
sendHandlerError(res, error);
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const executeMatch = req.url?.match(/^\/actions\/([^/]+)\/execute$/);
|
|
741
|
+
if (req.method === "POST" && executeMatch) {
|
|
742
|
+
if (!requireCallbackAuthorized(req, res, opts.callbackToken)) return;
|
|
743
|
+
const name = decodeURIComponent(executeMatch[1]);
|
|
744
|
+
const action = (opts.actions ?? []).find((candidate) => candidate.name === name);
|
|
745
|
+
if (!action) {
|
|
746
|
+
send(res, 404, { code: "ACTION_NOT_FOUND", message: `action ${name} not found`, retryable: false });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const raw = await readBody(req);
|
|
750
|
+
let payload;
|
|
751
|
+
try {
|
|
752
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
753
|
+
} catch (error) {
|
|
754
|
+
send(res, 400, {
|
|
755
|
+
code: "VALIDATION_FAILED",
|
|
756
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
757
|
+
retryable: false
|
|
758
|
+
});
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const commandIdRaw = req.headers["x-xtrape-command-id"];
|
|
762
|
+
const commandId = Array.isArray(commandIdRaw) ? commandIdRaw[0] : commandIdRaw;
|
|
763
|
+
if (action.async === true && typeof commandId === "string" && commandId.length > 0 && agent) {
|
|
764
|
+
const reporter = agent;
|
|
765
|
+
send(res, 202, { contractVersion: CE_CONTRACT_VERSION2, status: "RUNNING", commandId });
|
|
766
|
+
void (async () => {
|
|
767
|
+
let outcome;
|
|
768
|
+
try {
|
|
769
|
+
const result = await action.execute(payload);
|
|
770
|
+
outcome = {
|
|
771
|
+
status: "COMPLETED",
|
|
772
|
+
...result !== void 0 && result !== null && typeof result === "object" && !Array.isArray(result) ? { result } : result !== void 0 ? { result: { value: result } } : {}
|
|
773
|
+
};
|
|
774
|
+
} catch (error) {
|
|
775
|
+
outcome = {
|
|
776
|
+
status: "FAILED",
|
|
777
|
+
errorCode: "ACTION_FAILED",
|
|
778
|
+
errorMessage: error instanceof Error ? error.message : "action handler failed"
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
await reporter.reportCommandResult(commandId, outcome);
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
})();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
send(res, 200, await action.execute(payload));
|
|
790
|
+
} catch (error) {
|
|
791
|
+
sendHandlerError(res, error);
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
624
795
|
send(res, 404, { code: "NOT_FOUND", message: "not found", retryable: false });
|
|
625
796
|
} catch (e) {
|
|
626
797
|
sendMessageError(res, e);
|
|
@@ -644,6 +815,7 @@ async function serveCeService(opts) {
|
|
|
644
815
|
address,
|
|
645
816
|
instanceId: opts.instanceId,
|
|
646
817
|
registrationToken: opts.registrationToken,
|
|
818
|
+
controlToken: opts.controlToken,
|
|
647
819
|
fetchImpl: opts.fetchImpl
|
|
648
820
|
});
|
|
649
821
|
await agent.register();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtrape/capsule-agent-node",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Node.js Embedded Agent SDK for reporting Workers to the Xtrape control plane.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xtrape",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"lint": "tsc --noEmit"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@xtrape/capsule-contracts-node": "^0.6.
|
|
47
|
+
"@xtrape/capsule-contracts-node": "^0.6.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"tsup": "^8.3.5",
|