@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 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 (34).**
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. |
@@ -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;
@@ -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.#status = 'ready';
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.12",
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.12"
50
+ "@impetik/xeer": "0.2.14"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^24.1.0"
@@ -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 ["dev [directory] [--host <host>] [--port <port>] [--json]"];
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";
@@ -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>] [--json]'], helpOrder: 220,
341
- outputProtocol: 'xeer.dev.v0', effects: ['read-source', 'write-generated', 'run-local', 'write-state'],
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]',
@@ -19,3 +19,4 @@ export * from './admin-sql.js';
19
19
  export * from './network-policy.js';
20
20
  export * from './review.js';
21
21
  export * from './template.js';
22
+ export * from './tunnel.js';
@@ -19,3 +19,4 @@ export * from './admin-sql.js';
19
19
  export * from './network-policy.js';
20
20
  export * from './review.js';
21
21
  export * from './template.js';
22
+ export * from './tunnel.js';
@@ -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
+ }