@impetik/xeer-mcp 0.2.12 → 0.2.14
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 +3 -2
- package/dist/dev-session.d.ts +6 -0
- package/dist/dev-session.js +20 -2
- package/dist/server.js +7 -1
- package/package.json +2 -2
- package/vendor/spec/actions.d.ts +49 -2
- package/vendor/spec/actions.js +60 -2
- package/vendor/spec/index.d.ts +1 -0
- package/vendor/spec/index.js +1 -0
- package/vendor/spec/tunnel.d.ts +109 -0
- package/vendor/spec/tunnel.js +228 -0
package/README.md
CHANGED
|
@@ -71,18 +71,19 @@ The generated registry is the complete MCP tool and exclusion surface:
|
|
|
71
71
|
| `xeer_promote` | `promote.operator` | `operator` | Promote an exact review receipt in operator mode. | `directory?`: `string`<br>`receiptId`: `string`<br>`controlUrl?`: `string` | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | A human must review the exact artifact and start xeer-mcp with XEER_MCP_PROFILE=operator. |
|
|
72
72
|
| `xeer_auth_status` | `auth.status` | `author` / `operator` | Report builder credential metadata. | `controlUrl?`: `string` | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | none |
|
|
73
73
|
| `xeer_inspect` | `inspect` | `author` / `operator` | Read a running application manifest. | `previewUrl`: `string`<br>`view?`: `manifest` / `state` / `logs` / `export`; default `manifest`<br>`after?`: `string` | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | none |
|
|
74
|
-
| `xeer_dev_start` | `dev.start` | `author` / `operator` | Start a local development session. | `directory?`: `string`<br>`host?`: `string`<br>`port?`: `integer` [0..65535]<br>`timeoutMilliseconds?`: `integer` [1000..600000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
|
|
74
|
+
| `xeer_dev_start` | `dev.start` | `author` / `operator` | Start a local development session. | `directory?`: `string`<br>`host?`: `string`<br>`port?`: `integer` [0..65535]<br>`share?`: `boolean`<br>`timeoutMilliseconds?`: `integer` [1000..600000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
|
|
75
75
|
| `xeer_dev_status` | `dev.status` | `author` / `operator` | Read new events from a local development session. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0`<br>`waitMilliseconds?`: `integer` [0..600000]; default `0`<br>`limit?`: `integer` [1..1000]; default `200` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
|
|
76
76
|
| `xeer_dev_stop` | `dev.stop` | `author` / `operator` | Stop a local development session and release its lease. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0` | `run-local` | writes; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
|
|
77
77
|
| `xeer_diagnostics` | `diagnostics` | `author` / `operator` | Explain one emitted diagnostic code. | `code`: `string` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | none |
|
|
78
78
|
|
|
79
|
-
**CLI actions intentionally excluded from MCP (
|
|
79
|
+
**CLI actions intentionally excluded from MCP (35).**
|
|
80
80
|
|
|
81
81
|
| CLI action | Action | Summary | Effects | Safety | Path policy | Output | Why no MCP tool |
|
|
82
82
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
83
83
|
| `xeer agent setup` | `agent.setup` | Install or verify project-confined agent adapters. | `read-source`<br>`write-source` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
84
84
|
| `xeer deploy` | `deploy` | Build and deploy an application artifact. | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | MCP exposes a separate preview-only deploy action; direct production deploy stays CLI-only. |
|
|
85
85
|
| `xeer promote` | `promote` | Promote a preview artifact to production. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | MCP promotion is a separate action available only when the server starts in operator profile. |
|
|
86
|
+
| `xeer tunnel` | `tunnel.start` | Expose a loopback HTTP server through an authenticated tunnel. | `run-local`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `none` | `xeer.tunnel.cli.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
86
87
|
| `xeer link` | `link` | Link a checkout to an application the builder owns. | `read-source`<br>`write-source`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
|
|
87
88
|
| `xeer deployments` | `deployments` | List deployment history for an application. | `network-read`<br>`read-state`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
88
89
|
| `xeer rollback` | `rollback` | Redeploy a previously deployed artifact. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
package/dist/dev-session.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface DevSessionSummary {
|
|
|
34
34
|
generation?: number;
|
|
35
35
|
artifactId?: string;
|
|
36
36
|
preview?: PreviewUrls;
|
|
37
|
+
tunnel?: {
|
|
38
|
+
url: string;
|
|
39
|
+
tunnelId: string;
|
|
40
|
+
leaseExpiresAt: string;
|
|
41
|
+
};
|
|
37
42
|
/** Diagnostics from the most recent compile attempt. Empty once a rebuild is accepted. */
|
|
38
43
|
openDiagnostics: Diagnostic[];
|
|
39
44
|
exit?: {
|
|
@@ -51,6 +56,7 @@ export interface DevSessionOptions {
|
|
|
51
56
|
directory: string;
|
|
52
57
|
host?: string;
|
|
53
58
|
port?: number;
|
|
59
|
+
share?: boolean;
|
|
54
60
|
}
|
|
55
61
|
export declare class DevSession {
|
|
56
62
|
#private;
|
package/dist/dev-session.js
CHANGED
|
@@ -21,6 +21,8 @@ export class DevSession {
|
|
|
21
21
|
#cursor = 0;
|
|
22
22
|
#status = 'starting';
|
|
23
23
|
#preview;
|
|
24
|
+
#tunnel;
|
|
25
|
+
#expectsTunnel;
|
|
24
26
|
#generation;
|
|
25
27
|
#artifactId;
|
|
26
28
|
#openDiagnostics = [];
|
|
@@ -35,10 +37,12 @@ export class DevSession {
|
|
|
35
37
|
constructor(sessionId, options) {
|
|
36
38
|
this.sessionId = sessionId;
|
|
37
39
|
this.directory = options.directory;
|
|
40
|
+
this.#expectsTunnel = options.share === true;
|
|
38
41
|
// Port 0 is the documented automation mode: the OS picks a free port and the
|
|
39
42
|
// preview.ready event reports it, so parallel sessions cannot collide.
|
|
40
43
|
const args = ['dev', options.directory, '--json',
|
|
41
|
-
'--host', options.host ?? '127.0.0.1', '--port', String(options.port ?? 0)
|
|
44
|
+
'--host', options.host ?? '127.0.0.1', '--port', String(options.port ?? 0),
|
|
45
|
+
...(options.share ? ['--share'] : [])];
|
|
42
46
|
this.#child = fork(resolveXeerCli(), args, {
|
|
43
47
|
cwd: options.directory,
|
|
44
48
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
@@ -140,9 +144,22 @@ export class DevSession {
|
|
|
140
144
|
break;
|
|
141
145
|
case 'preview.ready':
|
|
142
146
|
this.#preview = event.data;
|
|
143
|
-
this.#
|
|
147
|
+
if (!this.#expectsTunnel)
|
|
148
|
+
this.#status = 'ready';
|
|
144
149
|
this.#compiling = false;
|
|
145
150
|
break;
|
|
151
|
+
case 'tunnel.ready': {
|
|
152
|
+
const data = event.data;
|
|
153
|
+
if (typeof data.url === 'string' && typeof data.tunnelId === 'string'
|
|
154
|
+
&& typeof data.leaseExpiresAt === 'string') {
|
|
155
|
+
this.#tunnel = { url: data.url, tunnelId: data.tunnelId, leaseExpiresAt: data.leaseExpiresAt };
|
|
156
|
+
this.#status = 'ready';
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'tunnel.closed':
|
|
161
|
+
this.#tunnel = undefined;
|
|
162
|
+
break;
|
|
146
163
|
case 'process.exit': {
|
|
147
164
|
const data = event.data;
|
|
148
165
|
this.#exit = { ...(typeof data.code === 'number' ? { code: data.code } : {}), ...(data.reason ? { reason: data.reason } : {}) };
|
|
@@ -219,6 +236,7 @@ export class DevSession {
|
|
|
219
236
|
...(this.#generation === undefined ? {} : { generation: this.#generation }),
|
|
220
237
|
...(this.#artifactId === undefined ? {} : { artifactId: this.#artifactId }),
|
|
221
238
|
...(this.#preview ? { preview: this.#preview } : {}),
|
|
239
|
+
...(this.#tunnel ? { tunnel: this.#tunnel } : {}),
|
|
222
240
|
openDiagnostics: [...this.#openDiagnostics],
|
|
223
241
|
...(this.#exit ? { exit: this.#exit } : {}),
|
|
224
242
|
droppedEvents: this.#dropped,
|
package/dist/server.js
CHANGED
|
@@ -433,6 +433,11 @@ export function createXeerMcpServer(options = {}) {
|
|
|
433
433
|
debugUrl: z.string(),
|
|
434
434
|
logsUrl: z.string(),
|
|
435
435
|
}).loose().optional(),
|
|
436
|
+
tunnel: z.object({
|
|
437
|
+
url: z.string(),
|
|
438
|
+
tunnelId: z.string(),
|
|
439
|
+
leaseExpiresAt: z.string(),
|
|
440
|
+
}).loose().optional(),
|
|
436
441
|
openDiagnostics: z.array(diagnosticSchema),
|
|
437
442
|
exit: z.object({ code: z.number().optional(), reason: z.string().optional() }).loose().optional(),
|
|
438
443
|
droppedEvents: z.number(),
|
|
@@ -454,13 +459,14 @@ export function createXeerMcpServer(options = {}) {
|
|
|
454
459
|
server.registerTool('xeer_dev_start', {
|
|
455
460
|
...toolPolicy('dev.start'),
|
|
456
461
|
outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
|
|
457
|
-
}, async ({ directory, host, port, timeoutMilliseconds }) => withDirectory(directory, 'dev.start', async (resolved) => {
|
|
462
|
+
}, async ({ directory, host, port, share, timeoutMilliseconds }) => withDirectory(directory, 'dev.start', async (resolved) => {
|
|
458
463
|
let session;
|
|
459
464
|
try {
|
|
460
465
|
session = devSessions.start({
|
|
461
466
|
directory: resolved,
|
|
462
467
|
...(host ? { host } : {}),
|
|
463
468
|
...(port === undefined ? {} : { port }),
|
|
469
|
+
...(share === undefined ? {} : { share }),
|
|
464
470
|
});
|
|
465
471
|
}
|
|
466
472
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impetik/xeer-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Model Context Protocol server for Xeer: the scaffold, check, dev, build, and deploy loop as agent tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"zod": "^4.0.10",
|
|
50
|
-
"@impetik/xeer": "0.2.
|
|
50
|
+
"@impetik/xeer": "0.2.14"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^24.1.0"
|
package/vendor/spec/actions.d.ts
CHANGED
|
@@ -47,8 +47,12 @@ export interface XeerActionDefinition {
|
|
|
47
47
|
readonly command: readonly string[];
|
|
48
48
|
readonly aliases?: readonly (readonly string[])[];
|
|
49
49
|
readonly summary: string;
|
|
50
|
+
/** Optional command-specific detail rendered only by focused `xeer <command> --help`. */
|
|
51
|
+
readonly description?: string;
|
|
50
52
|
/** Help syntax without the `xeer ` prefix. May contain `{{templates}}`. */
|
|
51
53
|
readonly usage: readonly string[];
|
|
54
|
+
/** Optional abbreviated syntax for the global command list. Full usage remains authoritative. */
|
|
55
|
+
readonly globalUsage?: readonly string[];
|
|
52
56
|
readonly hiddenFromHelp?: boolean;
|
|
53
57
|
readonly helpOrder?: number;
|
|
54
58
|
readonly outputProtocol: string;
|
|
@@ -71,6 +75,15 @@ export interface XeerActionDefinition {
|
|
|
71
75
|
readonly mcpExclusion?: string;
|
|
72
76
|
};
|
|
73
77
|
}
|
|
78
|
+
export interface XeerCliCommandGroupDefinition {
|
|
79
|
+
readonly command: string;
|
|
80
|
+
readonly summary: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Human-facing summaries for command families. Membership and ordering still come from the action
|
|
84
|
+
* registry; this only supplies the sentence that no individual subcommand can describe honestly.
|
|
85
|
+
*/
|
|
86
|
+
export declare const XEER_CLI_COMMAND_GROUPS: readonly XeerCliCommandGroupDefinition[];
|
|
74
87
|
export interface XeerCliOptionDefinition {
|
|
75
88
|
readonly name: string;
|
|
76
89
|
readonly takesValue: boolean;
|
|
@@ -180,6 +193,7 @@ declare const actions: readonly [{
|
|
|
180
193
|
readonly id: "new";
|
|
181
194
|
readonly command: readonly ["new"];
|
|
182
195
|
readonly summary: "Create a new project from a supported scaffold.";
|
|
196
|
+
readonly description: string;
|
|
183
197
|
readonly usage: readonly ["new <directory> [--template {{templates}}] [--json]"];
|
|
184
198
|
readonly helpOrder: 30;
|
|
185
199
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -329,6 +343,7 @@ declare const actions: readonly [{
|
|
|
329
343
|
readonly id: "deploy";
|
|
330
344
|
readonly command: readonly ["deploy"];
|
|
331
345
|
readonly summary: "Build and deploy an application artifact.";
|
|
346
|
+
readonly description: string;
|
|
332
347
|
readonly usage: readonly ["deploy [directory] [--environment prod|preview] [--control-url <url>] [--json]"];
|
|
333
348
|
readonly helpOrder: 50;
|
|
334
349
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -346,6 +361,7 @@ declare const actions: readonly [{
|
|
|
346
361
|
readonly id: "promote";
|
|
347
362
|
readonly command: readonly ["promote"];
|
|
348
363
|
readonly summary: "Promote a preview artifact to production.";
|
|
364
|
+
readonly description: string;
|
|
349
365
|
readonly usage: readonly [string];
|
|
350
366
|
readonly helpOrder: 80;
|
|
351
367
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -464,6 +480,7 @@ declare const actions: readonly [{
|
|
|
464
480
|
readonly id: "inspect";
|
|
465
481
|
readonly command: readonly ["inspect"];
|
|
466
482
|
readonly summary: "Read a running application manifest.";
|
|
483
|
+
readonly description: string;
|
|
467
484
|
readonly usage: readonly ["inspect <local-url|app> [--environment prod|preview] [--control-url <url>] [--json]"];
|
|
468
485
|
readonly helpOrder: 230;
|
|
469
486
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -503,10 +520,10 @@ declare const actions: readonly [{
|
|
|
503
520
|
readonly id: "dev.start";
|
|
504
521
|
readonly command: readonly ["dev"];
|
|
505
522
|
readonly summary: "Start a local development session.";
|
|
506
|
-
readonly usage: readonly [
|
|
523
|
+
readonly usage: readonly [string];
|
|
507
524
|
readonly helpOrder: 220;
|
|
508
525
|
readonly outputProtocol: "xeer.dev.v0";
|
|
509
|
-
readonly effects: readonly ["read-source", "write-generated", "run-local", "write-state"];
|
|
526
|
+
readonly effects: readonly ["read-source", "write-generated", "run-local", "write-state", "network-read", "network-write"];
|
|
510
527
|
readonly idempotent: false;
|
|
511
528
|
readonly reversible: true;
|
|
512
529
|
readonly destructive: false;
|
|
@@ -530,6 +547,10 @@ declare const actions: readonly [{
|
|
|
530
547
|
readonly minimum: 0;
|
|
531
548
|
readonly maximum: 65535;
|
|
532
549
|
};
|
|
550
|
+
readonly share: {
|
|
551
|
+
readonly description: "Publish the session through an authenticated Xeer tunnel.";
|
|
552
|
+
readonly type: "boolean";
|
|
553
|
+
};
|
|
533
554
|
readonly timeoutMilliseconds: {
|
|
534
555
|
readonly type: "integer";
|
|
535
556
|
readonly minimum: 1000;
|
|
@@ -543,6 +564,24 @@ declare const actions: readonly [{
|
|
|
543
564
|
readonly mcpTool: "xeer_dev_start";
|
|
544
565
|
readonly mcpProfiles: readonly ["author", "operator"];
|
|
545
566
|
};
|
|
567
|
+
}, {
|
|
568
|
+
readonly id: "tunnel.start";
|
|
569
|
+
readonly command: readonly ["tunnel"];
|
|
570
|
+
readonly summary: "Expose a loopback HTTP server through an authenticated tunnel.";
|
|
571
|
+
readonly usage: readonly [string];
|
|
572
|
+
readonly globalUsage: readonly ["tunnel <port|loopback-url> [options]"];
|
|
573
|
+
readonly helpOrder: 225;
|
|
574
|
+
readonly outputProtocol: "xeer.tunnel.cli.v0";
|
|
575
|
+
readonly effects: readonly ["run-local", "network-read", "network-write"];
|
|
576
|
+
readonly idempotent: false;
|
|
577
|
+
readonly reversible: true;
|
|
578
|
+
readonly destructive: false;
|
|
579
|
+
readonly humanPrerequisites: readonly ["A human must sign in and deliberately keep the public tunnel process running."];
|
|
580
|
+
readonly pathPolicy: "none";
|
|
581
|
+
readonly surfaces: {
|
|
582
|
+
readonly cli: true;
|
|
583
|
+
readonly mcpExclusion: "Not exposed through MCP v0; use the CLI deliberately.";
|
|
584
|
+
};
|
|
546
585
|
}, {
|
|
547
586
|
readonly id: "dev.status";
|
|
548
587
|
readonly command: readonly ["dev"];
|
|
@@ -676,6 +715,7 @@ declare const actions: readonly [{
|
|
|
676
715
|
readonly id: "deployments";
|
|
677
716
|
readonly command: readonly ["deployments"];
|
|
678
717
|
readonly summary: "List deployment history for an application.";
|
|
718
|
+
readonly description: string;
|
|
679
719
|
readonly usage: readonly ["deployments [app|directory] [--limit <n>] [--control-url <url>] [--json]"];
|
|
680
720
|
readonly helpOrder: 70;
|
|
681
721
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -693,6 +733,7 @@ declare const actions: readonly [{
|
|
|
693
733
|
readonly id: "rollback";
|
|
694
734
|
readonly command: readonly ["rollback"];
|
|
695
735
|
readonly summary: "Redeploy a previously deployed artifact.";
|
|
736
|
+
readonly description: string;
|
|
696
737
|
readonly usage: readonly ["rollback [app|directory] <artifactId> [--environment prod|preview] [--control-url <url>] [--json]"];
|
|
697
738
|
readonly helpOrder: 90;
|
|
698
739
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -710,6 +751,7 @@ declare const actions: readonly [{
|
|
|
710
751
|
readonly id: "disable";
|
|
711
752
|
readonly command: readonly ["disable"];
|
|
712
753
|
readonly summary: "Take an application offline without deleting its data.";
|
|
754
|
+
readonly description: string;
|
|
713
755
|
readonly usage: readonly ["disable [app|directory] [--control-url <url>] [--json]"];
|
|
714
756
|
readonly helpOrder: 100;
|
|
715
757
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -744,6 +786,7 @@ declare const actions: readonly [{
|
|
|
744
786
|
readonly id: "delete";
|
|
745
787
|
readonly command: readonly ["delete"];
|
|
746
788
|
readonly summary: "Permanently delete an application and all of its data.";
|
|
789
|
+
readonly description: string;
|
|
747
790
|
readonly usage: readonly ["delete [app|directory] --confirm <application> [--control-url <url>] [--json]"];
|
|
748
791
|
readonly helpOrder: 120;
|
|
749
792
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -848,6 +891,7 @@ declare const actions: readonly [{
|
|
|
848
891
|
readonly id: "state.read";
|
|
849
892
|
readonly command: readonly ["state"];
|
|
850
893
|
readonly summary: "Read record counts from a running application.";
|
|
894
|
+
readonly description: string;
|
|
851
895
|
readonly usage: readonly ["state <local-url|app> [--environment prod|preview] [--control-url <url>] [--json]"];
|
|
852
896
|
readonly helpOrder: 260;
|
|
853
897
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -882,6 +926,7 @@ declare const actions: readonly [{
|
|
|
882
926
|
readonly id: "logs";
|
|
883
927
|
readonly command: readonly ["logs"];
|
|
884
928
|
readonly summary: "Read the bounded log ring of a running application.";
|
|
929
|
+
readonly description: string;
|
|
885
930
|
readonly usage: readonly ["logs <local-url|app> [--environment prod|preview] [--after <cursor>] [--control-url <url>] [--json]"];
|
|
886
931
|
readonly helpOrder: 280;
|
|
887
932
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -899,6 +944,7 @@ declare const actions: readonly [{
|
|
|
899
944
|
readonly id: "export";
|
|
900
945
|
readonly command: readonly ["export"];
|
|
901
946
|
readonly summary: "Export application state to a document.";
|
|
947
|
+
readonly description: string;
|
|
902
948
|
readonly usage: readonly ["export <local-url|app> [--environment prod|preview] [--out <file>] [--control-url <url>] [--json]", "export --state <dev|preview> [directory] [--out <file>] [--json]"];
|
|
903
949
|
readonly helpOrder: 290;
|
|
904
950
|
readonly outputProtocol: "xeer.command.v0";
|
|
@@ -916,6 +962,7 @@ declare const actions: readonly [{
|
|
|
916
962
|
readonly id: "import";
|
|
917
963
|
readonly command: readonly ["import"];
|
|
918
964
|
readonly summary: "Replace local or preview state from an export document.";
|
|
965
|
+
readonly description: string;
|
|
919
966
|
readonly usage: readonly ["import <preview-url> <file> [--json]", "import --state <dev|preview> [directory] <file> [--json]"];
|
|
920
967
|
readonly helpOrder: 300;
|
|
921
968
|
readonly outputProtocol: "xeer.command.v0";
|
package/vendor/spec/actions.js
CHANGED
|
@@ -4,6 +4,20 @@ export const ACTION_REGISTRY_PROTOCOL = 'xeer.actions.v0';
|
|
|
4
4
|
export const XEER_MCP_PROFILES = ['author', 'operator'];
|
|
5
5
|
export const XEER_ARTIFACT_ID_PATTERN = '^sha256:[a-f0-9]{64}$';
|
|
6
6
|
export const MCP_INPUT_SCHEMA_DRAFT = 'http://json-schema.org/draft-07/schema#';
|
|
7
|
+
/**
|
|
8
|
+
* Human-facing summaries for command families. Membership and ordering still come from the action
|
|
9
|
+
* registry; this only supplies the sentence that no individual subcommand can describe honestly.
|
|
10
|
+
*/
|
|
11
|
+
export const XEER_CLI_COMMAND_GROUPS = Object.freeze([
|
|
12
|
+
{ command: 'agent', summary: 'Set up agent integrations and inspect normalized project context.' },
|
|
13
|
+
{ command: 'docs', summary: 'Search the documentation bundled with this Xeer version.' },
|
|
14
|
+
{ command: 'auth', summary: 'Manage builder credentials and local development personas.' },
|
|
15
|
+
{ command: 'domains', summary: 'Manage an application\'s platform and custom domains.' },
|
|
16
|
+
{ command: 'state', summary: 'Inspect running application state or reset local state.' },
|
|
17
|
+
{ command: 'env', summary: 'Manage application environment values and secrets.' },
|
|
18
|
+
{ command: 'token', summary: 'Manage service builder tokens.' },
|
|
19
|
+
{ command: 'db', summary: 'Inspect and query a running application database.' },
|
|
20
|
+
]);
|
|
7
21
|
const NOT_EXPOSED_V0 = 'Not exposed through MCP v0; use the CLI deliberately.';
|
|
8
22
|
const HUMAN_IDENTITY = 'Human identity and project ownership decisions are not delegated through MCP.';
|
|
9
23
|
const SECRET_VALUES = 'Secret values are intentionally unreachable through MCP; env pull returns plaintext.';
|
|
@@ -179,6 +193,9 @@ const actions = [
|
|
|
179
193
|
},
|
|
180
194
|
{
|
|
181
195
|
id: 'new', command: ['new'], summary: 'Create a new project from a supported scaffold.',
|
|
196
|
+
description: 'Creates a complete project that checks, tests, and builds clean. Templates do not '
|
|
197
|
+
+ 'scaffold sign-in UI: every visitor already has a verified identity, and the generated README '
|
|
198
|
+
+ 'explains how to opt in to a persistent account.',
|
|
182
199
|
usage: ['new <directory> [--template {{templates}}] [--json]'], helpOrder: 30,
|
|
183
200
|
outputProtocol: 'xeer.command.v0', effects: ['write-source'], idempotent: false,
|
|
184
201
|
reversible: true, destructive: false, humanPrerequisites: [], pathPolicy: 'project-relative',
|
|
@@ -250,6 +267,8 @@ const actions = [
|
|
|
250
267
|
},
|
|
251
268
|
{
|
|
252
269
|
id: 'deploy', command: ['deploy'], summary: 'Build and deploy an application artifact.',
|
|
270
|
+
description: 'Deploys to production by default. Use --environment preview for a separate preview '
|
|
271
|
+
+ 'Worker, URL, environment values, and disposable data so you can review a release before it goes live.',
|
|
253
272
|
usage: ['deploy [directory] [--environment prod|preview] [--control-url <url>] [--json]'], helpOrder: 50,
|
|
254
273
|
outputProtocol: 'xeer.command.v0',
|
|
255
274
|
effects: ['read-source', 'write-generated', 'run-local', 'network-read', 'network-write', 'production-change'],
|
|
@@ -259,6 +278,9 @@ const actions = [
|
|
|
259
278
|
},
|
|
260
279
|
{
|
|
261
280
|
id: 'promote', command: ['promote'], summary: 'Promote a preview artifact to production.',
|
|
281
|
+
description: 'Makes the exact artifact serving in preview live in production without rebuilding it. '
|
|
282
|
+
+ '--receipt uses the immutable review record from a preview deploy; --from-artifact is the direct '
|
|
283
|
+
+ 'human override for a retained artifact.',
|
|
262
284
|
usage: ['promote [app|directory] [--receipt <receiptId> | --from-artifact <artifactId>] '
|
|
263
285
|
+ '[--control-url <url>] [--json]'], helpOrder: 80,
|
|
264
286
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'network-write', 'production-change'],
|
|
@@ -315,6 +337,9 @@ const actions = [
|
|
|
315
337
|
},
|
|
316
338
|
{
|
|
317
339
|
id: 'inspect', command: ['inspect'], summary: 'Read a running application manifest.',
|
|
340
|
+
description: 'Reads a local dev or preview URL directly. For a deployed application, Xeer uses the '
|
|
341
|
+
+ 'signed-in builder credential and defaults names and app IDs to production; pass an exact URL or '
|
|
342
|
+
+ '--environment preview to select preview.',
|
|
318
343
|
usage: ['inspect <local-url|app> [--environment prod|preview] [--control-url <url>] [--json]'], helpOrder: 230,
|
|
319
344
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'read-state'], idempotent: true,
|
|
320
345
|
reversible: true, destructive: false, humanPrerequisites: [], pathPolicy: 'app-or-url',
|
|
@@ -337,8 +362,9 @@ const actions = [
|
|
|
337
362
|
},
|
|
338
363
|
{
|
|
339
364
|
id: 'dev.start', command: ['dev'], summary: 'Start a local development session.',
|
|
340
|
-
usage: ['dev [directory] [--host <host>] [--port <port>] [--
|
|
341
|
-
|
|
365
|
+
usage: ['dev [directory] [--host <host>] [--port <port>] [--share] [--control-url <url>] '
|
|
366
|
+
+ '[--tunnel-bootstrap <file>] [--json]'], helpOrder: 220,
|
|
367
|
+
outputProtocol: 'xeer.dev.v0', effects: ['read-source', 'write-generated', 'run-local', 'write-state', 'network-read', 'network-write'],
|
|
342
368
|
idempotent: false, reversible: true, destructive: false, humanPrerequisites: [],
|
|
343
369
|
pathPolicy: 'project-relative', surfaces: {
|
|
344
370
|
cli: true, mcpTool: 'xeer_dev_start', mcpProfiles: XEER_MCP_PROFILES,
|
|
@@ -347,10 +373,24 @@ const actions = [
|
|
|
347
373
|
directory: PROJECT_DIRECTORY_INPUT,
|
|
348
374
|
host: { description: 'Defaults to 127.0.0.1.', type: 'string' },
|
|
349
375
|
port: { description: 'Defaults to 0 (OS-assigned).', type: 'integer', minimum: 0, maximum: 65_535 },
|
|
376
|
+
share: { description: 'Publish the session through an authenticated Xeer tunnel.', type: 'boolean' },
|
|
350
377
|
timeoutMilliseconds: { type: 'integer', minimum: 1_000, maximum: 600_000 },
|
|
351
378
|
} },
|
|
352
379
|
},
|
|
353
380
|
},
|
|
381
|
+
{
|
|
382
|
+
id: 'tunnel.start', command: ['tunnel'], summary: 'Expose a loopback HTTP server through an authenticated tunnel.',
|
|
383
|
+
usage: ['tunnel <port|loopback-url> [--label <name>] [--host-header <preserve|rewrite|host>] '
|
|
384
|
+
+ '[--request-header-add <name:value>] [--request-header-remove <name>] '
|
|
385
|
+
+ '[--response-header-add <name:value>] [--response-header-remove <name>] '
|
|
386
|
+
+ '[--upstream-tls-insecure] [--lifetime <seconds>] [--control-url <url>] [--json]'],
|
|
387
|
+
globalUsage: ['tunnel <port|loopback-url> [options]'],
|
|
388
|
+
helpOrder: 225, outputProtocol: 'xeer.tunnel.cli.v0',
|
|
389
|
+
effects: ['run-local', 'network-read', 'network-write'], idempotent: false,
|
|
390
|
+
reversible: true, destructive: false,
|
|
391
|
+
humanPrerequisites: ['A human must sign in and deliberately keep the public tunnel process running.'],
|
|
392
|
+
pathPolicy: 'none', surfaces: { cli: true, mcpExclusion: NOT_EXPOSED_V0 },
|
|
393
|
+
},
|
|
354
394
|
{
|
|
355
395
|
id: 'dev.status', command: ['dev'], summary: 'Read new events from a local development session.',
|
|
356
396
|
usage: [], outputProtocol: 'xeer.dev.v0', effects: [], idempotent: true, reversible: true,
|
|
@@ -408,6 +448,8 @@ const actions = [
|
|
|
408
448
|
},
|
|
409
449
|
{
|
|
410
450
|
id: 'deployments', command: ['deployments'], summary: 'List deployment history for an application.',
|
|
451
|
+
description: 'Lists the newest deployments first and marks the version serving each environment. '
|
|
452
|
+
+ 'With no target, it uses the application linked to the current directory.',
|
|
411
453
|
usage: ['deployments [app|directory] [--limit <n>] [--control-url <url>] [--json]'], helpOrder: 70,
|
|
412
454
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'read-state', 'secret-metadata'],
|
|
413
455
|
idempotent: true, reversible: true, destructive: false,
|
|
@@ -416,6 +458,8 @@ const actions = [
|
|
|
416
458
|
},
|
|
417
459
|
{
|
|
418
460
|
id: 'rollback', command: ['rollback'], summary: 'Redeploy a previously deployed artifact.',
|
|
461
|
+
description: 'Creates a new deployment from a retained artifact instead of rewriting history. Use the '
|
|
462
|
+
+ 'artifact ID from xeer deployments; current environment values are injected at deployment time.',
|
|
419
463
|
usage: ['rollback [app|directory] <artifactId> [--environment prod|preview] [--control-url <url>] [--json]'],
|
|
420
464
|
helpOrder: 90, outputProtocol: 'xeer.command.v0',
|
|
421
465
|
effects: ['network-read', 'network-write', 'production-change'], idempotent: false,
|
|
@@ -425,6 +469,8 @@ const actions = [
|
|
|
425
469
|
},
|
|
426
470
|
{
|
|
427
471
|
id: 'disable', command: ['disable'], summary: 'Take an application offline without deleting its data.',
|
|
472
|
+
description: 'Returns 410 Gone for application requests while preserving the Worker, stored data, and '
|
|
473
|
+
+ 'environment values. Run xeer enable to restore service.',
|
|
428
474
|
usage: ['disable [app|directory] [--control-url <url>] [--json]'], helpOrder: 100,
|
|
429
475
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'network-write', 'production-change'],
|
|
430
476
|
idempotent: true, reversible: true, destructive: false,
|
|
@@ -441,6 +487,8 @@ const actions = [
|
|
|
441
487
|
},
|
|
442
488
|
{
|
|
443
489
|
id: 'delete', command: ['delete'], summary: 'Permanently delete an application and all of its data.',
|
|
490
|
+
description: 'Destroys the deployed Worker and stored data, stops the origin resolving, and permanently '
|
|
491
|
+
+ 'retires the app ID. The exact application name is required with --confirm.',
|
|
444
492
|
usage: ['delete [app|directory] --confirm <application> [--control-url <url>] [--json]'], helpOrder: 120,
|
|
445
493
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'network-write', 'write-state', 'production-change'],
|
|
446
494
|
idempotent: false, reversible: false, destructive: true,
|
|
@@ -490,6 +538,9 @@ const actions = [
|
|
|
490
538
|
},
|
|
491
539
|
{
|
|
492
540
|
id: 'state.read', command: ['state'], summary: 'Read record counts from a running application.',
|
|
541
|
+
description: 'Reads a local dev or preview URL directly. For a deployed application, Xeer uses the '
|
|
542
|
+
+ 'signed-in builder credential and defaults names and app IDs to production; pass an exact URL or '
|
|
543
|
+
+ '--environment preview to select preview.',
|
|
493
544
|
usage: ['state <local-url|app> [--environment prod|preview] [--control-url <url>] [--json]'], helpOrder: 260,
|
|
494
545
|
outputProtocol: 'xeer.command.v0', effects: ['network-read', 'read-state'], idempotent: true,
|
|
495
546
|
reversible: true, destructive: false, humanPrerequisites: [], pathPolicy: 'app-or-url',
|
|
@@ -505,6 +556,9 @@ const actions = [
|
|
|
505
556
|
},
|
|
506
557
|
{
|
|
507
558
|
id: 'logs', command: ['logs'], summary: 'Read the bounded log ring of a running application.',
|
|
559
|
+
description: 'Reads a local dev or preview URL directly. For a deployed application, Xeer uses the '
|
|
560
|
+
+ 'signed-in builder credential and defaults names and app IDs to production; pass an exact URL or '
|
|
561
|
+
+ '--environment preview to select preview.',
|
|
508
562
|
usage: ['logs <local-url|app> [--environment prod|preview] [--after <cursor>] [--control-url <url>] [--json]'],
|
|
509
563
|
helpOrder: 280, outputProtocol: 'xeer.command.v0', effects: ['network-read', 'read-state'],
|
|
510
564
|
idempotent: true, reversible: true, destructive: false, humanPrerequisites: [], pathPolicy: 'app-or-url',
|
|
@@ -512,6 +566,8 @@ const actions = [
|
|
|
512
566
|
},
|
|
513
567
|
{
|
|
514
568
|
id: 'export', command: ['export'], summary: 'Export application state to a document.',
|
|
569
|
+
description: 'Reads a local URL directly or a deployed application through the signed-in builder. '
|
|
570
|
+
+ 'Use --state to export from the xeer dev or xeer preview server running for a project directory.',
|
|
515
571
|
usage: [
|
|
516
572
|
'export <local-url|app> [--environment prod|preview] [--out <file>] [--control-url <url>] [--json]',
|
|
517
573
|
'export --state <dev|preview> [directory] [--out <file>] [--json]',
|
|
@@ -522,6 +578,8 @@ const actions = [
|
|
|
522
578
|
},
|
|
523
579
|
{
|
|
524
580
|
id: 'import', command: ['import'], summary: 'Replace local or preview state from an export document.',
|
|
581
|
+
description: 'Replaces the selected state wholesale. Direct URLs must point to preview; --state imports '
|
|
582
|
+
+ 'into the xeer dev or xeer preview server running for a project directory. Production import is refused.',
|
|
525
583
|
usage: [
|
|
526
584
|
'import <preview-url> <file> [--json]',
|
|
527
585
|
'import --state <dev|preview> [directory] <file> [--json]',
|
package/vendor/spec/index.d.ts
CHANGED
package/vendor/spec/index.js
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export declare const TUNNEL_PROTOCOL: "xeer.tunnel.v0";
|
|
2
|
+
export declare const TUNNEL_GRANT_AUDIENCE: "xeer-tunnel";
|
|
3
|
+
export declare const TUNNEL_GRANT_TYP: "xeer-tunnel-grant+jws";
|
|
4
|
+
export declare const TUNNEL_FALLBACK_AUDIENCE: "xeer-tunnel-fallback";
|
|
5
|
+
export declare const TUNNEL_FALLBACK_TYP: "xeer-tunnel-fallback+jws";
|
|
6
|
+
export declare const TUNNEL_TICKET_HEADER: "x-xeer-tunnel-ticket";
|
|
7
|
+
export declare const TUNNEL_CONTROL_HEADER: "x-xeer-tunnel-control";
|
|
8
|
+
export declare const TUNNEL_SHARED_HEADER: "x-xeer-shared-dev";
|
|
9
|
+
export declare const TUNNEL_SHARED_HOST_HEADER: "x-xeer-shared-host";
|
|
10
|
+
export declare const TUNNEL_PROFILES: readonly ["shared-dev", "raw-http"];
|
|
11
|
+
export type TunnelProfile = (typeof TUNNEL_PROFILES)[number];
|
|
12
|
+
export declare const TUNNEL_BINARY_HEADER_BYTES = 12;
|
|
13
|
+
export declare const TUNNEL_MAX_FRAME_PAYLOAD = 65536;
|
|
14
|
+
export type TunnelBinaryOpcode = 0x01 | 0x02 | 0x03;
|
|
15
|
+
export interface TunnelBinaryFrame {
|
|
16
|
+
readonly opcode: TunnelBinaryOpcode;
|
|
17
|
+
readonly final: boolean;
|
|
18
|
+
readonly streamId: number;
|
|
19
|
+
/** Starts at zero and advances independently per sender and stream. */
|
|
20
|
+
readonly seq: number;
|
|
21
|
+
readonly payload: Uint8Array;
|
|
22
|
+
}
|
|
23
|
+
export interface TunnelLimits {
|
|
24
|
+
readonly maxBandwidthBytes: number;
|
|
25
|
+
readonly maxLifetimeSeconds: number;
|
|
26
|
+
readonly maxConcurrentStreams: number;
|
|
27
|
+
readonly connectionWindowBytes: number;
|
|
28
|
+
readonly streamWindowBytes: number;
|
|
29
|
+
readonly streamIdleSeconds: number;
|
|
30
|
+
}
|
|
31
|
+
export interface TunnelAgentBootstrap {
|
|
32
|
+
readonly protocol: typeof TUNNEL_PROTOCOL;
|
|
33
|
+
readonly profile: TunnelProfile;
|
|
34
|
+
readonly tunnelId: string;
|
|
35
|
+
readonly url: string;
|
|
36
|
+
readonly resumeSecret: string;
|
|
37
|
+
readonly connect: {
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly ticket: string;
|
|
40
|
+
readonly ticketExpiresAt: string;
|
|
41
|
+
};
|
|
42
|
+
readonly leaseExpiresAt: string;
|
|
43
|
+
readonly issuerEpoch: number;
|
|
44
|
+
readonly limits: TunnelLimits;
|
|
45
|
+
}
|
|
46
|
+
export interface TunnelAllocation extends TunnelAgentBootstrap {
|
|
47
|
+
readonly slug: string;
|
|
48
|
+
readonly controlSecret: string;
|
|
49
|
+
}
|
|
50
|
+
export type TunnelControlFrame = {
|
|
51
|
+
readonly t: 'hello';
|
|
52
|
+
readonly protocol: typeof TUNNEL_PROTOCOL;
|
|
53
|
+
readonly tunnelId: string;
|
|
54
|
+
readonly takeover: boolean;
|
|
55
|
+
} | {
|
|
56
|
+
readonly t: 'ready';
|
|
57
|
+
readonly tunnelId: string;
|
|
58
|
+
readonly leaseExpiresAt: string;
|
|
59
|
+
readonly limits: TunnelLimits;
|
|
60
|
+
readonly credit: {
|
|
61
|
+
readonly connectionBytes: number;
|
|
62
|
+
readonly streamBytes: number;
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
readonly t: 'open';
|
|
66
|
+
readonly streamId: number;
|
|
67
|
+
readonly method: string;
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
70
|
+
} | {
|
|
71
|
+
readonly t: 'upgrade';
|
|
72
|
+
readonly streamId: number;
|
|
73
|
+
readonly path: string;
|
|
74
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
75
|
+
readonly protocols: readonly string[];
|
|
76
|
+
} | {
|
|
77
|
+
readonly t: 'head';
|
|
78
|
+
readonly streamId: number;
|
|
79
|
+
readonly status: number;
|
|
80
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
81
|
+
} | {
|
|
82
|
+
readonly t: 'upgrade.result';
|
|
83
|
+
readonly streamId: number;
|
|
84
|
+
readonly accepted: boolean;
|
|
85
|
+
readonly protocol?: string;
|
|
86
|
+
readonly headers?: readonly (readonly [string, string])[];
|
|
87
|
+
} | {
|
|
88
|
+
readonly t: 'end';
|
|
89
|
+
readonly streamId: number;
|
|
90
|
+
} | {
|
|
91
|
+
readonly t: 'ws.close';
|
|
92
|
+
readonly streamId: number;
|
|
93
|
+
readonly code: number;
|
|
94
|
+
readonly reason: string;
|
|
95
|
+
} | {
|
|
96
|
+
readonly t: 'window.update';
|
|
97
|
+
readonly streamId: number;
|
|
98
|
+
readonly bytes: number;
|
|
99
|
+
} | {
|
|
100
|
+
readonly t: 'reset';
|
|
101
|
+
readonly streamId: number;
|
|
102
|
+
readonly reason: TunnelResetReason;
|
|
103
|
+
};
|
|
104
|
+
export type TunnelResetReason = 'visitor_disconnect' | 'agent_error' | 'agent_refused' | 'revoked' | 'lease_expired' | 'slow_consumer' | 'flow_control_violation' | 'protocol_error' | 'replaced';
|
|
105
|
+
export declare function encodeTunnelBinaryFrame(frame: TunnelBinaryFrame): Uint8Array;
|
|
106
|
+
export declare function decodeTunnelBinaryFrame(input: ArrayBuffer | ArrayBufferView): TunnelBinaryFrame;
|
|
107
|
+
export declare function parseTunnelControlFrame(input: string): TunnelControlFrame;
|
|
108
|
+
export declare function sanitizeTunnelRequestHeaders(headers: Headers, profile?: TunnelProfile): Array<[string, string]>;
|
|
109
|
+
export declare function sanitizeTunnelResponseHeaders(headers: readonly (readonly [string, string])[], profile?: TunnelProfile): Headers;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export const TUNNEL_PROTOCOL = 'xeer.tunnel.v0';
|
|
2
|
+
export const TUNNEL_GRANT_AUDIENCE = 'xeer-tunnel';
|
|
3
|
+
export const TUNNEL_GRANT_TYP = 'xeer-tunnel-grant+jws';
|
|
4
|
+
export const TUNNEL_FALLBACK_AUDIENCE = 'xeer-tunnel-fallback';
|
|
5
|
+
export const TUNNEL_FALLBACK_TYP = 'xeer-tunnel-fallback+jws';
|
|
6
|
+
export const TUNNEL_TICKET_HEADER = 'x-xeer-tunnel-ticket';
|
|
7
|
+
export const TUNNEL_CONTROL_HEADER = 'x-xeer-tunnel-control';
|
|
8
|
+
export const TUNNEL_SHARED_HEADER = 'x-xeer-shared-dev';
|
|
9
|
+
export const TUNNEL_SHARED_HOST_HEADER = 'x-xeer-shared-host';
|
|
10
|
+
export const TUNNEL_PROFILES = ['shared-dev', 'raw-http'];
|
|
11
|
+
export const TUNNEL_BINARY_HEADER_BYTES = 12;
|
|
12
|
+
export const TUNNEL_MAX_FRAME_PAYLOAD = 65_536;
|
|
13
|
+
function uint32(value, field) {
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) {
|
|
15
|
+
throw new RangeError(`${field} must be a uint32.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function encodeTunnelBinaryFrame(frame) {
|
|
19
|
+
uint32(frame.streamId, 'streamId');
|
|
20
|
+
if (frame.streamId === 0)
|
|
21
|
+
throw new RangeError('streamId zero is reserved for connection control.');
|
|
22
|
+
uint32(frame.seq, 'seq');
|
|
23
|
+
if (![0x01, 0x02, 0x03].includes(frame.opcode))
|
|
24
|
+
throw new RangeError('Unknown tunnel binary opcode.');
|
|
25
|
+
if (frame.payload.byteLength > TUNNEL_MAX_FRAME_PAYLOAD) {
|
|
26
|
+
throw new RangeError(`Tunnel payload exceeds ${TUNNEL_MAX_FRAME_PAYLOAD} bytes.`);
|
|
27
|
+
}
|
|
28
|
+
const encoded = new Uint8Array(TUNNEL_BINARY_HEADER_BYTES + frame.payload.byteLength);
|
|
29
|
+
const view = new DataView(encoded.buffer);
|
|
30
|
+
view.setUint8(0, 0);
|
|
31
|
+
view.setUint8(1, frame.opcode);
|
|
32
|
+
view.setUint8(2, frame.final ? 1 : 0);
|
|
33
|
+
view.setUint8(3, 0);
|
|
34
|
+
view.setUint32(4, frame.streamId);
|
|
35
|
+
view.setUint32(8, frame.seq);
|
|
36
|
+
encoded.set(frame.payload, TUNNEL_BINARY_HEADER_BYTES);
|
|
37
|
+
return encoded;
|
|
38
|
+
}
|
|
39
|
+
export function decodeTunnelBinaryFrame(input) {
|
|
40
|
+
const bytes = input instanceof ArrayBuffer
|
|
41
|
+
? new Uint8Array(input)
|
|
42
|
+
: new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
43
|
+
if (bytes.byteLength < TUNNEL_BINARY_HEADER_BYTES)
|
|
44
|
+
throw new RangeError('Tunnel binary frame is truncated.');
|
|
45
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
46
|
+
if (view.getUint8(0) !== 0 || view.getUint8(3) !== 0 || (view.getUint8(2) & 0xfe) !== 0) {
|
|
47
|
+
throw new RangeError('Tunnel binary frame header is invalid.');
|
|
48
|
+
}
|
|
49
|
+
const opcode = view.getUint8(1);
|
|
50
|
+
if (opcode !== 0x01 && opcode !== 0x02 && opcode !== 0x03) {
|
|
51
|
+
throw new RangeError('Unknown tunnel binary opcode.');
|
|
52
|
+
}
|
|
53
|
+
const streamId = view.getUint32(4);
|
|
54
|
+
if (streamId === 0)
|
|
55
|
+
throw new RangeError('streamId zero is reserved for connection control.');
|
|
56
|
+
const payload = bytes.slice(TUNNEL_BINARY_HEADER_BYTES);
|
|
57
|
+
if (payload.byteLength > TUNNEL_MAX_FRAME_PAYLOAD)
|
|
58
|
+
throw new RangeError('Tunnel binary payload is too large.');
|
|
59
|
+
return { opcode, final: view.getUint8(2) === 1, streamId, seq: view.getUint32(8), payload };
|
|
60
|
+
}
|
|
61
|
+
export function parseTunnelControlFrame(input) {
|
|
62
|
+
let value;
|
|
63
|
+
try {
|
|
64
|
+
value = JSON.parse(input);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new TypeError('Tunnel control frame is not JSON.');
|
|
68
|
+
}
|
|
69
|
+
if (!value || typeof value !== 'object' || typeof value.t !== 'string') {
|
|
70
|
+
throw new TypeError('Tunnel control frame has no type.');
|
|
71
|
+
}
|
|
72
|
+
const frame = value;
|
|
73
|
+
const streamId = (allowZero = false) => {
|
|
74
|
+
if (!Number.isSafeInteger(frame.streamId) || frame.streamId < (allowZero ? 0 : 1)
|
|
75
|
+
|| frame.streamId > 0xffff_ffff)
|
|
76
|
+
invalidControlFrame();
|
|
77
|
+
};
|
|
78
|
+
const text = (field, maximum = 8_192, allowEmpty = false) => {
|
|
79
|
+
const candidate = frame[field];
|
|
80
|
+
if (typeof candidate !== 'string' || (!allowEmpty && candidate.length === 0)
|
|
81
|
+
|| candidate.length > maximum)
|
|
82
|
+
invalidControlFrame();
|
|
83
|
+
};
|
|
84
|
+
const headers = (field = 'headers', optional = false) => {
|
|
85
|
+
const candidate = frame[field];
|
|
86
|
+
if (optional && candidate === undefined)
|
|
87
|
+
return;
|
|
88
|
+
if (!Array.isArray(candidate) || candidate.some((header) => !Array.isArray(header)
|
|
89
|
+
|| header.length !== 2 || typeof header[0] !== 'string' || typeof header[1] !== 'string'
|
|
90
|
+
|| !HEADER_NAME.test(header[0]) || /[\0\r\n]/u.test(header[1]))) {
|
|
91
|
+
invalidControlFrame();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
switch (frame.t) {
|
|
95
|
+
case 'hello':
|
|
96
|
+
text('tunnelId', 256);
|
|
97
|
+
if (frame.protocol !== TUNNEL_PROTOCOL || typeof frame.takeover !== 'boolean')
|
|
98
|
+
invalidControlFrame();
|
|
99
|
+
break;
|
|
100
|
+
case 'ready': {
|
|
101
|
+
text('tunnelId', 256);
|
|
102
|
+
text('leaseExpiresAt', 64);
|
|
103
|
+
if (Number.isNaN(Date.parse(frame.leaseExpiresAt)) || !validLimits(frame.limits)
|
|
104
|
+
|| !frame.credit || typeof frame.credit !== 'object')
|
|
105
|
+
invalidControlFrame();
|
|
106
|
+
const credit = frame.credit;
|
|
107
|
+
if (!positiveInteger(credit.connectionBytes) || !positiveInteger(credit.streamBytes))
|
|
108
|
+
invalidControlFrame();
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'open':
|
|
112
|
+
streamId();
|
|
113
|
+
text('method', 32);
|
|
114
|
+
text('path');
|
|
115
|
+
headers();
|
|
116
|
+
if (!frame.path.startsWith('/'))
|
|
117
|
+
invalidControlFrame();
|
|
118
|
+
break;
|
|
119
|
+
case 'upgrade':
|
|
120
|
+
streamId();
|
|
121
|
+
text('path');
|
|
122
|
+
headers();
|
|
123
|
+
if (!frame.path.startsWith('/') || !Array.isArray(frame.protocols)
|
|
124
|
+
|| frame.protocols.some((protocol) => typeof protocol !== 'string' || protocol.length > 256)) {
|
|
125
|
+
invalidControlFrame();
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case 'head':
|
|
129
|
+
streamId();
|
|
130
|
+
headers();
|
|
131
|
+
if (!Number.isSafeInteger(frame.status) || frame.status < 100
|
|
132
|
+
|| frame.status > 599)
|
|
133
|
+
invalidControlFrame();
|
|
134
|
+
break;
|
|
135
|
+
case 'upgrade.result':
|
|
136
|
+
streamId();
|
|
137
|
+
headers('headers', true);
|
|
138
|
+
if (typeof frame.accepted !== 'boolean'
|
|
139
|
+
|| (frame.protocol !== undefined && (typeof frame.protocol !== 'string' || frame.protocol.length > 256))) {
|
|
140
|
+
invalidControlFrame();
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case 'end':
|
|
144
|
+
streamId();
|
|
145
|
+
break;
|
|
146
|
+
case 'ws.close':
|
|
147
|
+
streamId();
|
|
148
|
+
text('reason', 123, true);
|
|
149
|
+
if (!validWebSocketCloseCode(frame.code)
|
|
150
|
+
|| new TextEncoder().encode(frame.reason).byteLength > 123)
|
|
151
|
+
invalidControlFrame();
|
|
152
|
+
break;
|
|
153
|
+
case 'window.update':
|
|
154
|
+
streamId(true);
|
|
155
|
+
if (!positiveInteger(frame.bytes))
|
|
156
|
+
invalidControlFrame();
|
|
157
|
+
break;
|
|
158
|
+
case 'reset':
|
|
159
|
+
streamId();
|
|
160
|
+
if (!RESET_REASONS.has(frame.reason))
|
|
161
|
+
invalidControlFrame();
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
invalidControlFrame();
|
|
165
|
+
}
|
|
166
|
+
return frame;
|
|
167
|
+
}
|
|
168
|
+
const RESET_REASONS = new Set([
|
|
169
|
+
'visitor_disconnect', 'agent_error', 'agent_refused', 'revoked', 'lease_expired',
|
|
170
|
+
'slow_consumer', 'flow_control_violation', 'protocol_error', 'replaced',
|
|
171
|
+
]);
|
|
172
|
+
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
|
|
173
|
+
function invalidControlFrame() {
|
|
174
|
+
throw new TypeError('Tunnel control frame is invalid.');
|
|
175
|
+
}
|
|
176
|
+
function positiveInteger(value) {
|
|
177
|
+
return Number.isSafeInteger(value) && value > 0 && value <= 0xffff_ffff;
|
|
178
|
+
}
|
|
179
|
+
function positiveSafeInteger(value) {
|
|
180
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
181
|
+
}
|
|
182
|
+
function validLimits(value) {
|
|
183
|
+
if (!value || typeof value !== 'object')
|
|
184
|
+
return false;
|
|
185
|
+
const limits = value;
|
|
186
|
+
return ['maxBandwidthBytes', 'maxLifetimeSeconds', 'maxConcurrentStreams', 'connectionWindowBytes',
|
|
187
|
+
'streamWindowBytes', 'streamIdleSeconds'].every((field) => positiveSafeInteger(limits[field]));
|
|
188
|
+
}
|
|
189
|
+
function validWebSocketCloseCode(value) {
|
|
190
|
+
return Number.isSafeInteger(value)
|
|
191
|
+
&& ((value >= 1000 && value <= 1014) || value >= 3000)
|
|
192
|
+
&& value <= 4999
|
|
193
|
+
&& ![1004, 1005, 1006, 1015].includes(value);
|
|
194
|
+
}
|
|
195
|
+
const HOP_BY_HOP = new Set([
|
|
196
|
+
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer',
|
|
197
|
+
'transfer-encoding', 'upgrade',
|
|
198
|
+
]);
|
|
199
|
+
export function sanitizeTunnelRequestHeaders(headers, profile = 'shared-dev') {
|
|
200
|
+
const connectionNamed = new Set((headers.get('connection') ?? '').split(',').map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const [rawName, value] of headers) {
|
|
203
|
+
const name = rawName.toLowerCase();
|
|
204
|
+
if ((profile === 'shared-dev' && (name === 'authorization' || name === 'cookie'))
|
|
205
|
+
|| name === 'forwarded'
|
|
206
|
+
|| name === 'x-real-ip' || name.startsWith('x-forwarded-')
|
|
207
|
+
|| name.startsWith('cf-')
|
|
208
|
+
|| name.startsWith('x-xeer-') || name.startsWith('xeer-')
|
|
209
|
+
|| name.startsWith('proxy-') || HOP_BY_HOP.has(name) || connectionNamed.has(name))
|
|
210
|
+
continue;
|
|
211
|
+
result.push([name, value]);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
export function sanitizeTunnelResponseHeaders(headers, profile = 'shared-dev') {
|
|
216
|
+
const connectionNamed = new Set(headers.filter(([name]) => name.toLowerCase() === 'connection')
|
|
217
|
+
.flatMap(([, value]) => value.split(',')).map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
218
|
+
const result = new Headers();
|
|
219
|
+
for (const [rawName, value] of headers) {
|
|
220
|
+
const name = rawName.toLowerCase();
|
|
221
|
+
if ((profile === 'shared-dev' && name === 'set-cookie') || name.startsWith('proxy-')
|
|
222
|
+
|| HOP_BY_HOP.has(name) || connectionNamed.has(name))
|
|
223
|
+
continue;
|
|
224
|
+
result.append(name, value);
|
|
225
|
+
}
|
|
226
|
+
result.set('x-robots-tag', 'noindex');
|
|
227
|
+
return result;
|
|
228
|
+
}
|