@peekdev/mcp 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +10 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +7 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +37 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +86 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_initial.sql +102 -0
- package/dist/db/migrations/0002_network_bodies.sql +15 -0
- package/dist/db/open.d.ts +57 -0
- package/dist/db/open.d.ts.map +1 -0
- package/dist/db/open.js +74 -0
- package/dist/db/open.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/action-schema.d.ts +223 -0
- package/dist/mcp/action-schema.d.ts.map +1 -0
- package/dist/mcp/action-schema.js +97 -0
- package/dist/mcp/action-schema.js.map +1 -0
- package/dist/mcp/event-blobs.d.ts +32 -0
- package/dist/mcp/event-blobs.d.ts.map +1 -0
- package/dist/mcp/event-blobs.js +65 -0
- package/dist/mcp/event-blobs.js.map +1 -0
- package/dist/mcp/event-walker.d.ts +86 -0
- package/dist/mcp/event-walker.d.ts.map +1 -0
- package/dist/mcp/event-walker.js +398 -0
- package/dist/mcp/event-walker.js.map +1 -0
- package/dist/mcp/host-bridge.d.ts +80 -0
- package/dist/mcp/host-bridge.d.ts.map +1 -0
- package/dist/mcp/host-bridge.js +88 -0
- package/dist/mcp/host-bridge.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +32 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/playwright-repro.d.ts +19 -0
- package/dist/mcp/playwright-repro.d.ts.map +1 -0
- package/dist/mcp/playwright-repro.js +78 -0
- package/dist/mcp/playwright-repro.js.map +1 -0
- package/dist/mcp/queries.d.ts +73 -0
- package/dist/mcp/queries.d.ts.map +1 -0
- package/dist/mcp/queries.js +139 -0
- package/dist/mcp/queries.js.map +1 -0
- package/dist/mcp/roots.d.ts +50 -0
- package/dist/mcp/roots.d.ts.map +1 -0
- package/dist/mcp/roots.js +97 -0
- package/dist/mcp/roots.js.map +1 -0
- package/dist/mcp/rrweb-types.d.ts +3 -0
- package/dist/mcp/rrweb-types.d.ts.map +1 -0
- package/dist/mcp/rrweb-types.js +7 -0
- package/dist/mcp/rrweb-types.js.map +1 -0
- package/dist/mcp/selector.d.ts +54 -0
- package/dist/mcp/selector.d.ts.map +1 -0
- package/dist/mcp/selector.js +209 -0
- package/dist/mcp/selector.js.map +1 -0
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +469 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/summary.d.ts +26 -0
- package/dist/mcp/summary.d.ts.map +1 -0
- package/dist/mcp/summary.js +74 -0
- package/dist/mcp/summary.js.map +1 -0
- package/dist/native-host/action-protocol.d.ts +49 -0
- package/dist/native-host/action-protocol.d.ts.map +1 -0
- package/dist/native-host/action-protocol.js +36 -0
- package/dist/native-host/action-protocol.js.map +1 -0
- package/dist/native-host/audit.d.ts +69 -0
- package/dist/native-host/audit.d.ts.map +1 -0
- package/dist/native-host/audit.js +85 -0
- package/dist/native-host/audit.js.map +1 -0
- package/dist/native-host/config.d.ts +18 -0
- package/dist/native-host/config.d.ts.map +1 -0
- package/dist/native-host/config.js +56 -0
- package/dist/native-host/config.js.map +1 -0
- package/dist/native-host/extension-ids.json +6 -0
- package/dist/native-host/host.d.ts +30 -0
- package/dist/native-host/host.d.ts.map +1 -0
- package/dist/native-host/host.js +96 -0
- package/dist/native-host/host.js.map +1 -0
- package/dist/native-host/index.d.ts +4 -0
- package/dist/native-host/index.d.ts.map +1 -0
- package/dist/native-host/index.js +8 -0
- package/dist/native-host/index.js.map +1 -0
- package/dist/native-host/ingest.d.ts +83 -0
- package/dist/native-host/ingest.d.ts.map +1 -0
- package/dist/native-host/ingest.js +283 -0
- package/dist/native-host/ingest.js.map +1 -0
- package/dist/native-host/installer.d.ts +64 -0
- package/dist/native-host/installer.d.ts.map +1 -0
- package/dist/native-host/installer.js +110 -0
- package/dist/native-host/installer.js.map +1 -0
- package/dist/native-host/manifest.d.ts +64 -0
- package/dist/native-host/manifest.d.ts.map +1 -0
- package/dist/native-host/manifest.js +117 -0
- package/dist/native-host/manifest.js.map +1 -0
- package/dist/native-host/policy.d.ts +60 -0
- package/dist/native-host/policy.d.ts.map +1 -0
- package/dist/native-host/policy.js +116 -0
- package/dist/native-host/policy.js.map +1 -0
- package/dist/native-host/request-registry.d.ts +55 -0
- package/dist/native-host/request-registry.d.ts.map +1 -0
- package/dist/native-host/request-registry.js +111 -0
- package/dist/native-host/request-registry.js.map +1 -0
- package/dist/native-host/transport.d.ts +54 -0
- package/dist/native-host/transport.d.ts.map +1 -0
- package/dist/native-host/transport.js +113 -0
- package/dist/native-host/transport.js.map +1 -0
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +72 -0
- package/dist/postinstall.js.map +1 -0
- package/package.json +59 -0
- package/src/db/migrations/0001_initial.sql +102 -0
- package/src/db/migrations/0002_network_bodies.sql +15 -0
- package/src/native-host/extension-ids.json +6 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Native-host manifest construction + install-target resolution (ADR-0007,
|
|
2
|
+
// P2 PRD §A7). Everything here is pure and parameterized over the platform,
|
|
3
|
+
// home directory, host-binary path, and extension IDs so the postinstall
|
|
4
|
+
// side-effects (filesystem / registry writes) can be unit-tested without
|
|
5
|
+
// touching the real OS.
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
/** Reverse-DNS native-host id (ADR-0009 / NAMING.md). */
|
|
8
|
+
export const NATIVE_HOST_NAME = 'com.cubenest.peek';
|
|
9
|
+
/** The manifest filename written into each NativeMessagingHosts directory. */
|
|
10
|
+
export const MANIFEST_FILENAME = `${NATIVE_HOST_NAME}.json`;
|
|
11
|
+
const PLACEHOLDER_PREFIX = 'PLACEHOLDER_';
|
|
12
|
+
/**
|
|
13
|
+
* Turn the three configured extension IDs into the `allowed_origins` array.
|
|
14
|
+
* Chrome forbids wildcards, so each id becomes an explicit
|
|
15
|
+
* `chrome-extension://<id>/` origin. Unconfigured placeholder ids are dropped
|
|
16
|
+
* (so a pre-publish install doesn't ship dead origins), de-duplicating the
|
|
17
|
+
* result.
|
|
18
|
+
*/
|
|
19
|
+
export function allowedOrigins(ids) {
|
|
20
|
+
const candidates = [ids.chromeWebStore, ids.edgeAddons, ids.dev];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const origins = [];
|
|
23
|
+
for (const id of candidates) {
|
|
24
|
+
if (!id || id.startsWith(PLACEHOLDER_PREFIX))
|
|
25
|
+
continue;
|
|
26
|
+
const origin = `chrome-extension://${id}/`;
|
|
27
|
+
if (seen.has(origin))
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(origin);
|
|
30
|
+
origins.push(origin);
|
|
31
|
+
}
|
|
32
|
+
return origins;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the native-host manifest. `hostBinaryPath` is the absolute path the
|
|
36
|
+
* browser will spawn over stdio (the installed `peek-mcp` bin, invoked with
|
|
37
|
+
* `--native-host`). Both Chrome and Edge ids live in the SAME manifest's
|
|
38
|
+
* `allowed_origins` per the Edge "first registry location wins" gotcha
|
|
39
|
+
* (P2 PRD §A7).
|
|
40
|
+
*/
|
|
41
|
+
export function buildManifest(hostBinaryPath, ids) {
|
|
42
|
+
return {
|
|
43
|
+
name: NATIVE_HOST_NAME,
|
|
44
|
+
description: 'peek local bridge — native messaging host for the peek browser companion',
|
|
45
|
+
path: hostBinaryPath,
|
|
46
|
+
type: 'stdio',
|
|
47
|
+
allowed_origins: allowedOrigins(ids),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the per-OS set of native-messaging install targets (P2 PRD §A7).
|
|
52
|
+
*
|
|
53
|
+
* - macOS: Chrome, Chromium, Edge `NativeMessagingHosts/` directories under
|
|
54
|
+
* `~/Library/Application Support/`.
|
|
55
|
+
* - Linux: Chrome + Chromium `NativeMessagingHosts/` under `~/.config/`.
|
|
56
|
+
* - Windows: HKCU registry keys for Chrome + Edge (the default value of each
|
|
57
|
+
* key is the on-disk manifest path).
|
|
58
|
+
*
|
|
59
|
+
* `homeDir` is injected for testability.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveInstallTargets(platform, homeDir) {
|
|
62
|
+
switch (platform) {
|
|
63
|
+
case 'darwin': {
|
|
64
|
+
const appSupport = join(homeDir, 'Library', 'Application Support');
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
browser: 'macOS Chrome',
|
|
68
|
+
manifestPath: join(appSupport, 'Google', 'Chrome', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
browser: 'macOS Chromium',
|
|
72
|
+
manifestPath: join(appSupport, 'Chromium', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
browser: 'macOS Edge',
|
|
76
|
+
manifestPath: join(appSupport, 'Microsoft Edge', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
case 'linux': {
|
|
81
|
+
const config = join(homeDir, '.config');
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
browser: 'Linux Chrome',
|
|
85
|
+
manifestPath: join(config, 'google-chrome', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
browser: 'Linux Chromium',
|
|
89
|
+
manifestPath: join(config, 'chromium', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
case 'win32': {
|
|
94
|
+
// Per P2 PRD §A7, the Windows install needs BOTH:
|
|
95
|
+
// - the manifest JSON on disk (Chrome/Edge resolve registry values to
|
|
96
|
+
// a file path), and
|
|
97
|
+
// - the registry key whose default value points at that path.
|
|
98
|
+
// The conventional location is %LOCALAPPDATA%\<vendor>\<browser>\
|
|
99
|
+
// NativeMessagingHosts\<host>.json. We derive %LOCALAPPDATA% from the
|
|
100
|
+
// injected `homeDir` (`C:\Users\<user>`) so tests don't depend on env.
|
|
101
|
+
const localAppData = join(homeDir, 'AppData', 'Local');
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
browser: 'Windows Chrome',
|
|
105
|
+
manifestPath: join(localAppData, 'Google', 'Chrome', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
106
|
+
registryKey: `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
browser: 'Windows Edge',
|
|
110
|
+
manifestPath: join(localAppData, 'Microsoft', 'Edge', 'NativeMessagingHosts', MANIFEST_FILENAME),
|
|
111
|
+
registryKey: `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/native-host/manifest.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,wBAAwB;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEpD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,gBAAgB,OAAO,CAAC;AAkB5D,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAiB;IAC9C,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC;YAAE,SAAS;QACvD,MAAM,MAAM,GAAG,sBAAsB,EAAE,GAAG,CAAC;QAC3C,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,SAAS;QAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,cAAsB,EAAE,GAAiB;IACrE,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,0EAA0E;QACvF,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,OAAO;QACb,eAAe,EAAE,cAAc,CAAC,GAAG,CAAC;KACrC,CAAC;AACJ,CAAC;AAqBD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAA2B,EAC3B,OAAe;IAEf,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAC;YACnE,OAAO;gBACL;oBACE,OAAO,EAAE,cAAc;oBACvB,YAAY,EAAE,IAAI,CAChB,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,sBAAsB,EACtB,iBAAiB,CAClB;iBACF;gBACD;oBACE,OAAO,EAAE,gBAAgB;oBACzB,YAAY,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,sBAAsB,EAAE,iBAAiB,CAAC;iBACtF;gBACD;oBACE,OAAO,EAAE,YAAY;oBACrB,YAAY,EAAE,IAAI,CAChB,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,CAClB;iBACF;aACF,CAAC;QACJ,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACxC,OAAO;gBACL;oBACE,OAAO,EAAE,cAAc;oBACvB,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,sBAAsB,EAAE,iBAAiB,CAAC;iBACvF;gBACD;oBACE,OAAO,EAAE,gBAAgB;oBACzB,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,sBAAsB,EAAE,iBAAiB,CAAC;iBAClF;aACF,CAAC;QACJ,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,kDAAkD;YAClD,wEAAwE;YACxE,wBAAwB;YACxB,gEAAgE;YAChE,kEAAkE;YAClE,sEAAsE;YACtE,uEAAuE;YACvE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;YACvD,OAAO;gBACL;oBACE,OAAO,EAAE,gBAAgB;oBACzB,YAAY,EAAE,IAAI,CAChB,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,sBAAsB,EACtB,iBAAiB,CAClB;oBACD,WAAW,EAAE,yDAAyD,gBAAgB,EAAE;iBACzF;gBACD;oBACE,OAAO,EAAE,cAAc;oBACvB,YAAY,EAAE,IAAI,CAChB,YAAY,EACZ,WAAW,EACX,MAAM,EACN,sBAAsB,EACtB,iBAAiB,CAClB;oBACD,WAAW,EAAE,0DAA0D,gBAAgB,EAAE;iBAC1F;aACF,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Default location of the user policy file. */
|
|
3
|
+
export declare function policyJsonPath(): string;
|
|
4
|
+
declare const PolicySchema: z.ZodObject<{
|
|
5
|
+
destructiveTerms: z.ZodOptional<z.ZodObject<{
|
|
6
|
+
add: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
7
|
+
remove: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
add?: unknown[] | undefined;
|
|
10
|
+
remove?: unknown[] | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
add?: unknown[] | undefined;
|
|
13
|
+
remove?: unknown[] | undefined;
|
|
14
|
+
}>>;
|
|
15
|
+
allowListBySite: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
destructiveTerms?: {
|
|
18
|
+
add?: unknown[] | undefined;
|
|
19
|
+
remove?: unknown[] | undefined;
|
|
20
|
+
} | undefined;
|
|
21
|
+
allowListBySite?: Record<string, unknown> | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
destructiveTerms?: {
|
|
24
|
+
add?: unknown[] | undefined;
|
|
25
|
+
remove?: unknown[] | undefined;
|
|
26
|
+
} | undefined;
|
|
27
|
+
allowListBySite?: Record<string, unknown> | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
export type PeekPolicy = z.infer<typeof PolicySchema>;
|
|
30
|
+
export interface DestructivePolicyDelta {
|
|
31
|
+
/** Terms the user wants to ADD to the matcher (lowercased + trimmed). */
|
|
32
|
+
readonly add: readonly string[];
|
|
33
|
+
/** Terms the user wants to REMOVE from the matcher. */
|
|
34
|
+
readonly remove: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/** A {origin → action types} allow-list (per P2 PRD §E.3 example). */
|
|
37
|
+
export interface AllowListBySite {
|
|
38
|
+
readonly [originPattern: string]: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export interface LoadedPolicy {
|
|
41
|
+
readonly destructiveTerms: DestructivePolicyDelta;
|
|
42
|
+
readonly allowListBySite: AllowListBySite;
|
|
43
|
+
}
|
|
44
|
+
/** Empty fallback returned when the file is missing / unreadable / malformed. */
|
|
45
|
+
export declare const EMPTY_POLICY: LoadedPolicy;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a raw JSON string into a normalized {@link LoadedPolicy}. Exported
|
|
48
|
+
* separately from `loadPolicy` so the unit tests can exercise the parsing
|
|
49
|
+
* surface without touching the filesystem.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parsePolicy(contents: string): LoadedPolicy;
|
|
52
|
+
/**
|
|
53
|
+
* Read + parse ~/.peek/policy.json. A missing file / read error / parse error
|
|
54
|
+
* resolves to {@link EMPTY_POLICY}.
|
|
55
|
+
*
|
|
56
|
+
* @param path override the policy path (tests + alternate PEEK_HOME).
|
|
57
|
+
*/
|
|
58
|
+
export declare function loadPolicy(path?: string): LoadedPolicy;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/native-host/policy.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,gDAAgD;AAChD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAOD,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;EAQhB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEtD,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,sEAAsE;AAGtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IAClD,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;CAC3C;AAED,iFAAiF;AACjF,eAAO,MAAM,YAAY,EAAE,YAG1B,CAAC;AA0BF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAgB1D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAyB,GAAG,YAAY,CAQxE"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// User policy file (`~/.peek/policy.json`) reader — ADR-0010 / P2 PRD §E.3.
|
|
2
|
+
//
|
|
3
|
+
// The hardcoded destructive blocklist ships in the extension; this module is
|
|
4
|
+
// the seam where users add / remove terms via a local JSON file. The native
|
|
5
|
+
// host reads this file once at boot (and on each action request — the file is
|
|
6
|
+
// tiny + the read is cheap), and forwards the {add, remove} deltas to the SW
|
|
7
|
+
// on every action.request so the MAIN-world dispatcher merges them at
|
|
8
|
+
// decision time.
|
|
9
|
+
//
|
|
10
|
+
// Schema:
|
|
11
|
+
// {
|
|
12
|
+
// "destructiveTerms": { "add": ["yeet", "nuke"], "remove": [] },
|
|
13
|
+
// "allowListBySite": { "https://example.com/*": ["click", "type"] }
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// Failure modes are all soft: a missing / unreadable / malformed file resolves
|
|
17
|
+
// to the empty policy. A loud throw here would lock action execution out
|
|
18
|
+
// entirely for users who haven't created the file. Same posture as
|
|
19
|
+
// activation/storage's `sanitize` (drop garbage, never throw).
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { peekHomeDir } from '../db/open.js';
|
|
24
|
+
/** Default location of the user policy file. */
|
|
25
|
+
export function policyJsonPath() {
|
|
26
|
+
return join(peekHomeDir(), 'policy.json');
|
|
27
|
+
}
|
|
28
|
+
// Permissive schema: arrays of `unknown` (sanitised post-parse) so a single
|
|
29
|
+
// non-string entry doesn't reject the entire add/remove list. The normaliser
|
|
30
|
+
// functions below filter mixed input down to clean strings — same posture as
|
|
31
|
+
// activation/storage's sanitize() (drop garbage, never throw, never lose the
|
|
32
|
+
// whole file to one bad entry).
|
|
33
|
+
const PolicySchema = z.object({
|
|
34
|
+
destructiveTerms: z
|
|
35
|
+
.object({
|
|
36
|
+
add: z.array(z.unknown()).optional(),
|
|
37
|
+
remove: z.array(z.unknown()).optional(),
|
|
38
|
+
})
|
|
39
|
+
.optional(),
|
|
40
|
+
allowListBySite: z.record(z.string(), z.unknown()).optional(),
|
|
41
|
+
});
|
|
42
|
+
/** Empty fallback returned when the file is missing / unreadable / malformed. */
|
|
43
|
+
export const EMPTY_POLICY = {
|
|
44
|
+
destructiveTerms: { add: [], remove: [] },
|
|
45
|
+
allowListBySite: {},
|
|
46
|
+
};
|
|
47
|
+
function normalizeTerms(value) {
|
|
48
|
+
if (!value)
|
|
49
|
+
return [];
|
|
50
|
+
const out = new Set();
|
|
51
|
+
for (const t of value) {
|
|
52
|
+
if (typeof t !== 'string')
|
|
53
|
+
continue;
|
|
54
|
+
const trimmed = t.trim().toLowerCase();
|
|
55
|
+
if (trimmed.length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
out.add(trimmed);
|
|
58
|
+
}
|
|
59
|
+
return [...out];
|
|
60
|
+
}
|
|
61
|
+
function normalizeAllowList(value) {
|
|
62
|
+
if (!value)
|
|
63
|
+
return {};
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [origin, types] of Object.entries(value)) {
|
|
66
|
+
if (typeof origin !== 'string' || origin.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
if (!Array.isArray(types))
|
|
69
|
+
continue;
|
|
70
|
+
const cleaned = types.filter((t) => typeof t === 'string' && t.length > 0);
|
|
71
|
+
if (cleaned.length > 0)
|
|
72
|
+
out[origin] = cleaned;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse a raw JSON string into a normalized {@link LoadedPolicy}. Exported
|
|
78
|
+
* separately from `loadPolicy` so the unit tests can exercise the parsing
|
|
79
|
+
* surface without touching the filesystem.
|
|
80
|
+
*/
|
|
81
|
+
export function parsePolicy(contents) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = JSON.parse(contents);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return EMPTY_POLICY;
|
|
88
|
+
}
|
|
89
|
+
const parsed = PolicySchema.safeParse(raw);
|
|
90
|
+
if (!parsed.success)
|
|
91
|
+
return EMPTY_POLICY;
|
|
92
|
+
return {
|
|
93
|
+
destructiveTerms: {
|
|
94
|
+
add: normalizeTerms(parsed.data.destructiveTerms?.add),
|
|
95
|
+
remove: normalizeTerms(parsed.data.destructiveTerms?.remove),
|
|
96
|
+
},
|
|
97
|
+
allowListBySite: normalizeAllowList(parsed.data.allowListBySite),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read + parse ~/.peek/policy.json. A missing file / read error / parse error
|
|
102
|
+
* resolves to {@link EMPTY_POLICY}.
|
|
103
|
+
*
|
|
104
|
+
* @param path override the policy path (tests + alternate PEEK_HOME).
|
|
105
|
+
*/
|
|
106
|
+
export function loadPolicy(path = policyJsonPath()) {
|
|
107
|
+
let contents;
|
|
108
|
+
try {
|
|
109
|
+
contents = readFileSync(path, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return EMPTY_POLICY;
|
|
113
|
+
}
|
|
114
|
+
return parsePolicy(contents);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/native-host/policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface PendingRequest<T> {
|
|
2
|
+
/** Resolve the awaiting tool handler with the SW's reply payload. */
|
|
3
|
+
resolve(value: T): void;
|
|
4
|
+
/** Reject the awaiting tool handler (timeout or transport error). */
|
|
5
|
+
reject(reason: unknown): void;
|
|
6
|
+
/** Timer handle so we can cancel on resolve / reject. */
|
|
7
|
+
timer: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface RequestRegistryDeps {
|
|
10
|
+
/** Generate a fresh, unique request id (UUID v4 in production). */
|
|
11
|
+
generateId(): string;
|
|
12
|
+
setTimeout(cb: () => void, ms: number): unknown;
|
|
13
|
+
clearTimeout(handle: unknown): void;
|
|
14
|
+
}
|
|
15
|
+
/** Default deps using node's globals + crypto.randomUUID. */
|
|
16
|
+
export declare const defaultRegistryDeps: RequestRegistryDeps;
|
|
17
|
+
/** Sentinel error thrown when a request times out without a reply. */
|
|
18
|
+
export declare class RequestTimeoutError extends Error {
|
|
19
|
+
readonly requestId: string;
|
|
20
|
+
readonly timeoutMs: number;
|
|
21
|
+
constructor(requestId: string, timeoutMs: number);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Tracks in-flight host→SW requests by id. The host's tool handler:
|
|
25
|
+
*
|
|
26
|
+
* const { id, response } = registry.create<MyReply>(5 * 60_000);
|
|
27
|
+
* await transport.send({ type: 'action.request', requestId: id, ... });
|
|
28
|
+
* return await response; // resolves on action.result, rejects on timeout
|
|
29
|
+
*
|
|
30
|
+
* The host's inbound-message handler calls `registry.resolve(id, payload)` or
|
|
31
|
+
* `registry.reject(id, err)` when the SW replies. Unknown ids are silently
|
|
32
|
+
* dropped (a stale reply after a timeout shouldn't crash the host).
|
|
33
|
+
*/
|
|
34
|
+
export declare class RequestRegistry {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(deps?: RequestRegistryDeps);
|
|
37
|
+
/**
|
|
38
|
+
* Allocate a request: returns the `id` and a `response` promise the caller
|
|
39
|
+
* awaits. The promise rejects with {@link RequestTimeoutError} after
|
|
40
|
+
* `timeoutMs` if no resolve/reject has arrived.
|
|
41
|
+
*/
|
|
42
|
+
create<T>(timeoutMs: number): {
|
|
43
|
+
id: string;
|
|
44
|
+
response: Promise<T>;
|
|
45
|
+
};
|
|
46
|
+
/** Resolve the request with `payload`. Unknown id → no-op (drop stale reply). */
|
|
47
|
+
resolve(id: string, payload: unknown): boolean;
|
|
48
|
+
/** Reject the request with `reason`. Unknown id → no-op. */
|
|
49
|
+
reject(id: string, reason: unknown): boolean;
|
|
50
|
+
/** Reject every pending request (used on transport teardown). */
|
|
51
|
+
rejectAll(reason: unknown): number;
|
|
52
|
+
/** Number of in-flight requests (diagnostics). */
|
|
53
|
+
get pendingCount(): number;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=request-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-registry.d.ts","sourceRoot":"","sources":["../../src/native-host/request-registry.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,qEAAqE;IACrE,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACxB,qEAAqE;IACrE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,UAAU,IAAI,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAED,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,EAAE,mBAIjC,CAAC;AAEF,sEAAsE;AACtE,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM;gBADjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM;CAK7B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;;gBAId,IAAI,GAAE,mBAAyC;IAI3D;;;;OAIG;IACH,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE;IAsBlE,iFAAiF;IACjF,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;IAS9C,4DAA4D;IAC5D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO;IAS5C,iEAAiE;IACjE,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM;IAUlC,kDAAkD;IAClD,IAAI,YAAY,IAAI,MAAM,CAEzB;CACF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Correlated-request registry for the MCP host (Task 3.24).
|
|
2
|
+
//
|
|
3
|
+
// `execute_action` and `request_authorization` are inherently asynchronous over
|
|
4
|
+
// the native port: the MCP tool handler sends an `action.request` to the SW,
|
|
5
|
+
// and the SW eventually replies with a `action.verdict` / `action.result`. The
|
|
6
|
+
// reply can take seconds (Level-3 banner waits on user click), and multiple
|
|
7
|
+
// requests can be in flight concurrently across MCP clients.
|
|
8
|
+
//
|
|
9
|
+
// We assign a UUID per request, store `{ resolve, reject, timer }` keyed by
|
|
10
|
+
// that id, and the host's inbound-message handler looks up the id and resolves
|
|
11
|
+
// the pending promise. Time out after `timeoutMs` so a wedged SW never leaves
|
|
12
|
+
// the MCP tool handler waiting forever.
|
|
13
|
+
//
|
|
14
|
+
// Pure JS — no `chrome.*`, no node-native deps — so it unit-tests cleanly.
|
|
15
|
+
/** Default deps using node's globals + crypto.randomUUID. */
|
|
16
|
+
export const defaultRegistryDeps = {
|
|
17
|
+
generateId: () => globalThis.crypto.randomUUID(),
|
|
18
|
+
setTimeout: (cb, ms) => setTimeout(cb, ms),
|
|
19
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
20
|
+
};
|
|
21
|
+
/** Sentinel error thrown when a request times out without a reply. */
|
|
22
|
+
export class RequestTimeoutError extends Error {
|
|
23
|
+
requestId;
|
|
24
|
+
timeoutMs;
|
|
25
|
+
constructor(requestId, timeoutMs) {
|
|
26
|
+
super(`peek: request ${requestId} timed out after ${timeoutMs}ms`);
|
|
27
|
+
this.requestId = requestId;
|
|
28
|
+
this.timeoutMs = timeoutMs;
|
|
29
|
+
this.name = 'RequestTimeoutError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tracks in-flight host→SW requests by id. The host's tool handler:
|
|
34
|
+
*
|
|
35
|
+
* const { id, response } = registry.create<MyReply>(5 * 60_000);
|
|
36
|
+
* await transport.send({ type: 'action.request', requestId: id, ... });
|
|
37
|
+
* return await response; // resolves on action.result, rejects on timeout
|
|
38
|
+
*
|
|
39
|
+
* The host's inbound-message handler calls `registry.resolve(id, payload)` or
|
|
40
|
+
* `registry.reject(id, err)` when the SW replies. Unknown ids are silently
|
|
41
|
+
* dropped (a stale reply after a timeout shouldn't crash the host).
|
|
42
|
+
*/
|
|
43
|
+
export class RequestRegistry {
|
|
44
|
+
#deps;
|
|
45
|
+
#pending = new Map();
|
|
46
|
+
constructor(deps = defaultRegistryDeps) {
|
|
47
|
+
this.#deps = deps;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Allocate a request: returns the `id` and a `response` promise the caller
|
|
51
|
+
* awaits. The promise rejects with {@link RequestTimeoutError} after
|
|
52
|
+
* `timeoutMs` if no resolve/reject has arrived.
|
|
53
|
+
*/
|
|
54
|
+
create(timeoutMs) {
|
|
55
|
+
const id = this.#deps.generateId();
|
|
56
|
+
let resolveFn;
|
|
57
|
+
let rejectFn;
|
|
58
|
+
const response = new Promise((resolve, reject) => {
|
|
59
|
+
resolveFn = resolve;
|
|
60
|
+
rejectFn = reject;
|
|
61
|
+
});
|
|
62
|
+
const timer = this.#deps.setTimeout(() => {
|
|
63
|
+
const pending = this.#pending.get(id);
|
|
64
|
+
if (!pending)
|
|
65
|
+
return; // already resolved/rejected
|
|
66
|
+
this.#pending.delete(id);
|
|
67
|
+
pending.reject(new RequestTimeoutError(id, timeoutMs));
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
this.#pending.set(id, {
|
|
70
|
+
resolve: resolveFn,
|
|
71
|
+
reject: rejectFn,
|
|
72
|
+
timer,
|
|
73
|
+
});
|
|
74
|
+
return { id, response };
|
|
75
|
+
}
|
|
76
|
+
/** Resolve the request with `payload`. Unknown id → no-op (drop stale reply). */
|
|
77
|
+
resolve(id, payload) {
|
|
78
|
+
const pending = this.#pending.get(id);
|
|
79
|
+
if (!pending)
|
|
80
|
+
return false;
|
|
81
|
+
this.#pending.delete(id);
|
|
82
|
+
this.#deps.clearTimeout(pending.timer);
|
|
83
|
+
pending.resolve(payload);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** Reject the request with `reason`. Unknown id → no-op. */
|
|
87
|
+
reject(id, reason) {
|
|
88
|
+
const pending = this.#pending.get(id);
|
|
89
|
+
if (!pending)
|
|
90
|
+
return false;
|
|
91
|
+
this.#pending.delete(id);
|
|
92
|
+
this.#deps.clearTimeout(pending.timer);
|
|
93
|
+
pending.reject(reason);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Reject every pending request (used on transport teardown). */
|
|
97
|
+
rejectAll(reason) {
|
|
98
|
+
const count = this.#pending.size;
|
|
99
|
+
for (const [id, pending] of this.#pending.entries()) {
|
|
100
|
+
this.#deps.clearTimeout(pending.timer);
|
|
101
|
+
pending.reject(reason);
|
|
102
|
+
this.#pending.delete(id);
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
/** Number of in-flight requests (diagnostics). */
|
|
107
|
+
get pendingCount() {
|
|
108
|
+
return this.#pending.size;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=request-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-registry.js","sourceRoot":"","sources":["../../src/native-host/request-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
/** Max bytes for a host -> extension message (1 MB), per Chrome/Edge spec. */
|
|
3
|
+
export declare const MAX_HOST_TO_EXT_BYTES: number;
|
|
4
|
+
/** Max bytes for an extension -> host message (4 GB), per Chrome/Edge spec. */
|
|
5
|
+
export declare const MAX_EXT_TO_HOST_BYTES: number;
|
|
6
|
+
/** Width of the little-endian length prefix. */
|
|
7
|
+
export declare const LENGTH_PREFIX_BYTES = 4;
|
|
8
|
+
/**
|
|
9
|
+
* Encode a value into a framed native-messaging buffer: a 4-byte little-endian
|
|
10
|
+
* length header followed by the UTF-8 JSON body.
|
|
11
|
+
*
|
|
12
|
+
* Throws if the encoded body exceeds the host -> extension 1 MB cap.
|
|
13
|
+
*/
|
|
14
|
+
export declare function encodeMessage(value: unknown): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* Write a single framed message to a stream (default `process.stdout`). The
|
|
17
|
+
* promise resolves once the bytes are flushed to the underlying handle.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeMessage(value: unknown, out?: Writable): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Streaming decoder for the inbound (extension -> host) direction. Feed it raw
|
|
22
|
+
* chunks as they arrive; it emits each fully-framed JSON message via the
|
|
23
|
+
* `onMessage` callback. Enforces the 4 GB ext -> host cap on the declared
|
|
24
|
+
* length before buffering the body.
|
|
25
|
+
*/
|
|
26
|
+
export interface MessageDecoderOptions {
|
|
27
|
+
/** Max declared frame length before the ext -> host cap guard throws. */
|
|
28
|
+
readonly maxBytes?: number;
|
|
29
|
+
/** Invoked when a frame body fails to JSON-parse; the frame is then skipped. */
|
|
30
|
+
readonly onError?: (error: Error) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare class MessageDecoder {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(onMessage: (message: unknown) => void, options?: MessageDecoderOptions);
|
|
35
|
+
/** Append a chunk and drain any complete messages it now contains. */
|
|
36
|
+
push(chunk: Buffer): void;
|
|
37
|
+
/** Number of bytes currently buffered awaiting a complete frame. */
|
|
38
|
+
get pending(): number;
|
|
39
|
+
}
|
|
40
|
+
export interface ReadMessagesOptions {
|
|
41
|
+
/** Stream to read from (default `process.stdin`). */
|
|
42
|
+
readonly input?: Readable;
|
|
43
|
+
/** Invoked when a malformed frame is skipped (does not reject the promise). */
|
|
44
|
+
readonly onError?: (error: Error) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read framed messages from a stream (default `process.stdin`), invoking
|
|
48
|
+
* `onMessage` for each. Resolves when the stream ends; rejects on a stream
|
|
49
|
+
* error or a frame that violates the size cap. A frame whose body fails to
|
|
50
|
+
* JSON-parse is skipped (reported via `onError`) rather than terminating the
|
|
51
|
+
* stream — a single malformed message from the extension must not kill the host.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readMessages(onMessage: (message: unknown) => void, options?: ReadMessagesOptions): Promise<void>;
|
|
54
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/native-host/transport.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,QAAc,CAAC;AAEjD,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,QAAyB,CAAC;AAE5D,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUpD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAE,QAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1F;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC3C;AAED,qBAAa,cAAc;;gBAOb,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EAAE,OAAO,GAAE,qBAA0B;IAMtF,sEAAsE;IACtE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAqCzB,oEAAoE;IACpE,IAAI,OAAO,IAAI,MAAM,CAEpB;CACF;AAED,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACrC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Chrome / Edge native-messaging transport (ADR-0007 action item 4).
|
|
2
|
+
//
|
|
3
|
+
// Framing: a 4-byte little-endian uint32 length header followed by that many
|
|
4
|
+
// bytes of UTF-8 JSON. Size caps come straight from the Microsoft Edge
|
|
5
|
+
// native-messaging docs (echoed for Chrome): a single message *from* the host
|
|
6
|
+
// to the extension may be at most 1 MB; a message *to* the host may be up to
|
|
7
|
+
// 4 GB. We enforce the 1 MB cap on write (host -> ext) and the 4 GB cap on
|
|
8
|
+
// read (ext -> host) so a misbehaving peer can't make us buffer unbounded.
|
|
9
|
+
/** Max bytes for a host -> extension message (1 MB), per Chrome/Edge spec. */
|
|
10
|
+
export const MAX_HOST_TO_EXT_BYTES = 1024 * 1024;
|
|
11
|
+
/** Max bytes for an extension -> host message (4 GB), per Chrome/Edge spec. */
|
|
12
|
+
export const MAX_EXT_TO_HOST_BYTES = 4 * 1024 * 1024 * 1024;
|
|
13
|
+
/** Width of the little-endian length prefix. */
|
|
14
|
+
export const LENGTH_PREFIX_BYTES = 4;
|
|
15
|
+
/**
|
|
16
|
+
* Encode a value into a framed native-messaging buffer: a 4-byte little-endian
|
|
17
|
+
* length header followed by the UTF-8 JSON body.
|
|
18
|
+
*
|
|
19
|
+
* Throws if the encoded body exceeds the host -> extension 1 MB cap.
|
|
20
|
+
*/
|
|
21
|
+
export function encodeMessage(value) {
|
|
22
|
+
const body = Buffer.from(JSON.stringify(value), 'utf8');
|
|
23
|
+
if (body.length > MAX_HOST_TO_EXT_BYTES) {
|
|
24
|
+
throw new Error(`native-messaging: message of ${body.length} bytes exceeds the ${MAX_HOST_TO_EXT_BYTES}-byte host->ext cap`);
|
|
25
|
+
}
|
|
26
|
+
const header = Buffer.allocUnsafe(LENGTH_PREFIX_BYTES);
|
|
27
|
+
header.writeUInt32LE(body.length, 0);
|
|
28
|
+
return Buffer.concat([header, body]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Write a single framed message to a stream (default `process.stdout`). The
|
|
32
|
+
* promise resolves once the bytes are flushed to the underlying handle.
|
|
33
|
+
*/
|
|
34
|
+
export function writeMessage(value, out = process.stdout) {
|
|
35
|
+
const framed = encodeMessage(value);
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
out.write(framed, (err) => (err ? reject(err) : resolve()));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export class MessageDecoder {
|
|
41
|
+
#buffer = Buffer.alloc(0);
|
|
42
|
+
#expectedLength = null;
|
|
43
|
+
#onMessage;
|
|
44
|
+
#maxBytes;
|
|
45
|
+
#onError;
|
|
46
|
+
constructor(onMessage, options = {}) {
|
|
47
|
+
this.#onMessage = onMessage;
|
|
48
|
+
this.#maxBytes = options.maxBytes ?? MAX_EXT_TO_HOST_BYTES;
|
|
49
|
+
this.#onError = options.onError;
|
|
50
|
+
}
|
|
51
|
+
/** Append a chunk and drain any complete messages it now contains. */
|
|
52
|
+
push(chunk) {
|
|
53
|
+
this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
|
|
54
|
+
// Loop because one chunk may complete multiple queued messages.
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (this.#expectedLength === null) {
|
|
57
|
+
if (this.#buffer.length < LENGTH_PREFIX_BYTES)
|
|
58
|
+
return;
|
|
59
|
+
const len = this.#buffer.readUInt32LE(0);
|
|
60
|
+
if (len > this.#maxBytes) {
|
|
61
|
+
throw new Error(`native-messaging: declared length ${len} exceeds the ${this.#maxBytes}-byte ext->host cap`);
|
|
62
|
+
}
|
|
63
|
+
this.#expectedLength = len;
|
|
64
|
+
this.#buffer = this.#buffer.subarray(LENGTH_PREFIX_BYTES);
|
|
65
|
+
}
|
|
66
|
+
if (this.#buffer.length < this.#expectedLength)
|
|
67
|
+
return;
|
|
68
|
+
const body = this.#buffer.subarray(0, this.#expectedLength);
|
|
69
|
+
this.#buffer = this.#buffer.subarray(this.#expectedLength);
|
|
70
|
+
this.#expectedLength = null;
|
|
71
|
+
// A single malformed frame must not kill the host loop: skip it (the
|
|
72
|
+
// frame was already consumed above, so the stream stays correctly aligned
|
|
73
|
+
// for the next length-prefixed message) and report it via onError.
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(body.toString('utf8'));
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
this.#onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
this.#onMessage(parsed);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Number of bytes currently buffered awaiting a complete frame. */
|
|
86
|
+
get pending() {
|
|
87
|
+
return this.#buffer.length;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read framed messages from a stream (default `process.stdin`), invoking
|
|
92
|
+
* `onMessage` for each. Resolves when the stream ends; rejects on a stream
|
|
93
|
+
* error or a frame that violates the size cap. A frame whose body fails to
|
|
94
|
+
* JSON-parse is skipped (reported via `onError`) rather than terminating the
|
|
95
|
+
* stream — a single malformed message from the extension must not kill the host.
|
|
96
|
+
*/
|
|
97
|
+
export function readMessages(onMessage, options = {}) {
|
|
98
|
+
const input = options.input ?? process.stdin;
|
|
99
|
+
const decoder = new MessageDecoder(onMessage, options.onError !== undefined ? { onError: options.onError } : {});
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
input.on('data', (chunk) => {
|
|
102
|
+
try {
|
|
103
|
+
decoder.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
reject(err);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
input.on('end', resolve);
|
|
110
|
+
input.on('error', reject);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=transport.js.map
|