@beeos-ai/device-agent 0.4.3 → 0.5.0
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/.pi/extensions/android-tools.ts +146 -15
- package/dist/adapters/agent-gateway-client.d.ts +37 -0
- package/dist/adapters/agent-gateway-client.js +45 -0
- package/dist/adapters/agent-gateway-client.js.map +1 -1
- package/dist/adapters/agentcore-pi.d.ts +22 -0
- package/dist/adapters/agentcore-pi.js +48 -0
- package/dist/adapters/agentcore-pi.js.map +1 -1
- package/dist/adapters/device-android-adb.d.ts +47 -0
- package/dist/adapters/device-android-adb.js +50 -0
- package/dist/adapters/device-android-adb.js.map +1 -1
- package/dist/adapters/device-desktop.d.ts +12 -0
- package/dist/adapters/device-desktop.js +31 -0
- package/dist/adapters/device-desktop.js.map +1 -1
- package/dist/agent-runtime/adapters/base.d.ts +20 -0
- package/dist/agent-runtime/adapters/base.js.map +1 -1
- package/dist/agent-runtime/agent-runner.d.ts +4 -2
- package/dist/agent-runtime/agent-runner.js.map +1 -1
- package/dist/agent-runtime/core.d.ts +60 -0
- package/dist/agent-runtime/core.js +190 -9
- package/dist/agent-runtime/core.js.map +1 -1
- package/dist/agent-runtime/feedback.d.ts +56 -0
- package/dist/agent-runtime/feedback.js +54 -0
- package/dist/agent-runtime/feedback.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +1 -0
- package/dist/agent-runtime/index.js +1 -0
- package/dist/agent-runtime/index.js.map +1 -1
- package/dist/agent-runtime/plan-executor.js +24 -0
- package/dist/agent-runtime/plan-executor.js.map +1 -1
- package/dist/agent-runtime/planner/base.d.ts +10 -0
- package/dist/agent-runtime/planner/base.js.map +1 -1
- package/dist/agent-runtime/planner/llm-planner.js +1 -0
- package/dist/agent-runtime/planner/llm-planner.js.map +1 -1
- package/dist/agent-runtime/planner/prompts.d.ts +3 -0
- package/dist/agent-runtime/planner/prompts.js +12 -0
- package/dist/agent-runtime/planner/prompts.js.map +1 -1
- package/dist/agent-runtime/reflection.d.ts +25 -6
- package/dist/agent-runtime/reflection.js +64 -22
- package/dist/agent-runtime/reflection.js.map +1 -1
- package/dist/agent-runtime/reflectors/policy.d.ts +10 -4
- package/dist/agent-runtime/reflectors/policy.js +19 -6
- package/dist/agent-runtime/reflectors/policy.js.map +1 -1
- package/dist/agent-runtime/shortcuts/fs-store.d.ts +41 -0
- package/dist/agent-runtime/shortcuts/fs-store.js +122 -0
- package/dist/agent-runtime/shortcuts/fs-store.js.map +1 -0
- package/dist/agent-runtime/shortcuts/in-memory-store.d.ts +20 -0
- package/dist/agent-runtime/shortcuts/in-memory-store.js +64 -0
- package/dist/agent-runtime/shortcuts/in-memory-store.js.map +1 -0
- package/dist/agent-runtime/shortcuts/index.d.ts +10 -0
- package/dist/agent-runtime/shortcuts/index.js +10 -0
- package/dist/agent-runtime/shortcuts/index.js.map +1 -0
- package/dist/agent-runtime/shortcuts/types.d.ts +69 -0
- package/dist/agent-runtime/shortcuts/types.js +17 -0
- package/dist/agent-runtime/shortcuts/types.js.map +1 -0
- package/dist/app/agent-host.d.ts +83 -0
- package/dist/app/agent-host.js +345 -32
- package/dist/app/agent-host.js.map +1 -1
- package/dist/app/session-manager.d.ts +33 -4
- package/dist/app/session-manager.js +27 -5
- package/dist/app/session-manager.js.map +1 -1
- package/dist/bootstrap.js +187 -3
- package/dist/bootstrap.js.map +1 -1
- package/dist/config/loader.js +22 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +118 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/inbound/acp/handlers/session.js.map +1 -1
- package/dist/mcp-client/client.d.ts +6 -0
- package/dist/mcp-client/client.js +11 -0
- package/dist/mcp-client/client.js.map +1 -1
- package/dist/mcp-client/runtime.d.ts +13 -0
- package/dist/orchestrator-system-prompt.d.ts +26 -0
- package/dist/orchestrator-system-prompt.js +118 -2
- package/dist/orchestrator-system-prompt.js.map +1 -1
- package/dist/pi-context.d.ts +50 -0
- package/dist/pi-context.js.map +1 -1
- package/dist/ports/device.d.ts +18 -0
- package/dist/resources/augmenter.d.ts +48 -0
- package/dist/resources/augmenter.js +126 -0
- package/dist/resources/augmenter.js.map +1 -0
- package/dist/resources/cache.d.ts +57 -0
- package/dist/resources/cache.js +107 -0
- package/dist/resources/cache.js.map +1 -0
- package/dist/resources/index.d.ts +17 -0
- package/dist/resources/index.js +15 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/lifecycle/disk-pressure.d.ts +75 -0
- package/dist/resources/lifecycle/disk-pressure.js +148 -0
- package/dist/resources/lifecycle/disk-pressure.js.map +1 -0
- package/dist/resources/lifecycle/index.d.ts +7 -0
- package/dist/resources/lifecycle/index.js +7 -0
- package/dist/resources/lifecycle/index.js.map +1 -0
- package/dist/resources/lifecycle/manager.d.ts +38 -0
- package/dist/resources/lifecycle/manager.js +205 -0
- package/dist/resources/lifecycle/manager.js.map +1 -0
- package/dist/resources/lifecycle/sweeper.d.ts +36 -0
- package/dist/resources/lifecycle/sweeper.js +179 -0
- package/dist/resources/lifecycle/sweeper.js.map +1 -0
- package/dist/resources/lifecycle/types.d.ts +180 -0
- package/dist/resources/lifecycle/types.js +46 -0
- package/dist/resources/lifecycle/types.js.map +1 -0
- package/dist/resources/parsers.d.ts +28 -0
- package/dist/resources/parsers.js +88 -0
- package/dist/resources/parsers.js.map +1 -0
- package/dist/resources/registry.d.ts +47 -0
- package/dist/resources/registry.js +107 -0
- package/dist/resources/registry.js.map +1 -0
- package/dist/resources/resolvers/beeos-file-resolver.d.ts +62 -0
- package/dist/resources/resolvers/beeos-file-resolver.js +109 -0
- package/dist/resources/resolvers/beeos-file-resolver.js.map +1 -0
- package/dist/resources/resolvers/errors.d.ts +11 -0
- package/dist/resources/resolvers/errors.js +23 -0
- package/dist/resources/resolvers/errors.js.map +1 -0
- package/dist/resources/resolvers/mime-table.d.ts +41 -0
- package/dist/resources/resolvers/mime-table.js +92 -0
- package/dist/resources/resolvers/mime-table.js.map +1 -0
- package/dist/resources/resolvers/url-resolver.d.ts +85 -0
- package/dist/resources/resolvers/url-resolver.js +495 -0
- package/dist/resources/resolvers/url-resolver.js.map +1 -0
- package/dist/resources/shortcut-facade.d.ts +51 -0
- package/dist/resources/shortcut-facade.js +72 -0
- package/dist/resources/shortcut-facade.js.map +1 -0
- package/dist/resources/telemetry.d.ts +52 -0
- package/dist/resources/telemetry.js +59 -0
- package/dist/resources/telemetry.js.map +1 -0
- package/dist/resources/types.d.ts +249 -0
- package/dist/resources/types.js +29 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/shortcuts/builtins/download-to-device.d.ts +83 -0
- package/dist/shortcuts/builtins/download-to-device.js +125 -0
- package/dist/shortcuts/builtins/download-to-device.js.map +1 -0
- package/dist/shortcuts/builtins/find-app-package.d.ts +79 -0
- package/dist/shortcuts/builtins/find-app-package.js +168 -0
- package/dist/shortcuts/builtins/find-app-package.js.map +1 -0
- package/dist/shortcuts/builtins/index.d.ts +32 -0
- package/dist/shortcuts/builtins/index.js +37 -0
- package/dist/shortcuts/builtins/index.js.map +1 -0
- package/dist/shortcuts/builtins/open-deeplink.d.ts +68 -0
- package/dist/shortcuts/builtins/open-deeplink.js +142 -0
- package/dist/shortcuts/builtins/open-deeplink.js.map +1 -0
- package/dist/shortcuts/builtins/set-clipboard.d.ts +65 -0
- package/dist/shortcuts/builtins/set-clipboard.js +112 -0
- package/dist/shortcuts/builtins/set-clipboard.js.map +1 -0
- package/dist/shortcuts/index.d.ts +11 -0
- package/dist/shortcuts/index.js +11 -0
- package/dist/shortcuts/index.js.map +1 -0
- package/dist/shortcuts/registry.d.ts +56 -0
- package/dist/shortcuts/registry.js +70 -0
- package/dist/shortcuts/registry.js.map +1 -0
- package/dist/shortcuts/router.d.ts +91 -0
- package/dist/shortcuts/router.js +158 -0
- package/dist/shortcuts/router.js.map +1 -0
- package/dist/shortcuts/types.d.ts +219 -0
- package/dist/shortcuts/types.js +46 -0
- package/dist/shortcuts/types.js.map +1 -0
- package/package.json +3 -3
- package/dist/agent-runtime/skills/sqlite-store.d.ts +0 -60
- package/dist/agent-runtime/skills/sqlite-store.js +0 -234
- package/dist/agent-runtime/skills/sqlite-store.js.map +0 -1
- package/dist/test-lan-autoglm.d.ts +0 -7
- package/dist/test-lan-autoglm.js +0 -46
- package/dist/test-lan-autoglm.js.map +0 -1
- package/dist/test-mem-breakdown.d.ts +0 -1
- package/dist/test-mem-breakdown.js +0 -103
- package/dist/test-mem-breakdown.js.map +0 -1
- package/dist/test-mem.d.ts +0 -1
- package/dist/test-mem.js +0 -60
- package/dist/test-mem.js.map +0 -1
- package/dist/test-mobile-use.d.ts +0 -1
- package/dist/test-mobile-use.js +0 -18
- package/dist/test-mobile-use.js.map +0 -1
- package/dist/test-screenshot-loop.d.ts +0 -1
- package/dist/test-screenshot-loop.js +0 -43
- package/dist/test-screenshot-loop.js.map +0 -1
- package/dist/test-screenshot.d.ts +0 -1
- package/dist/test-screenshot.js +0 -10
- package/dist/test-screenshot.js.map +0 -1
- package/dist/test-ui-xml-dump.d.ts +0 -15
- package/dist/test-ui-xml-dump.js +0 -49
- package/dist/test-ui-xml-dump.js.map +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Resolver Registry — owns the parser/resolver dispatch and
|
|
3
|
+
* the `resolveAll(task)` orchestration entry point.
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* 1. Bootstrap constructs one registry per AgentHost.
|
|
7
|
+
* 2. AgentHost.submitTask calls `parseAll(prompt)` to extract refs.
|
|
8
|
+
* 3. For each ref, registry picks the first matching `ResourceResolver`
|
|
9
|
+
* and delegates resolution. Parallel where possible.
|
|
10
|
+
* 4. Failures are isolated: one bad URL must not poison the rest of
|
|
11
|
+
* the task. The host falls back to the original prompt only when
|
|
12
|
+
* every resolution failed (or zero refs were found).
|
|
13
|
+
*
|
|
14
|
+
* The registry stays explicit about its dependencies (`ResolverContext`)
|
|
15
|
+
* so unit tests can wire it up without bootstrap.
|
|
16
|
+
*/
|
|
17
|
+
export class ResourceResolverRegistry {
|
|
18
|
+
parsers;
|
|
19
|
+
resolvers;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.parsers = [...opts.parsers];
|
|
22
|
+
this.resolvers = [...opts.resolvers];
|
|
23
|
+
}
|
|
24
|
+
/** Run every registered parser; deduplicate by JSON-canonical key. */
|
|
25
|
+
parseAll(task) {
|
|
26
|
+
if (!task)
|
|
27
|
+
return [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const p of this.parsers) {
|
|
31
|
+
let refs;
|
|
32
|
+
try {
|
|
33
|
+
refs = p.parse(task);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// A misbehaving parser must NOT crash the whole task.
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const ref of refs) {
|
|
40
|
+
const key = stableKey(ref);
|
|
41
|
+
if (seen.has(key))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
out.push(ref);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve every ref present in the task. Returns successfully
|
|
51
|
+
* resolved assets; failures are surfaced via the `errors` array but
|
|
52
|
+
* never thrown — the AgentHost is responsible for falling back to
|
|
53
|
+
* the original prompt when no asset survived.
|
|
54
|
+
*/
|
|
55
|
+
async resolveAll(task, ctx) {
|
|
56
|
+
const refs = this.parseAll(task);
|
|
57
|
+
if (refs.length === 0) {
|
|
58
|
+
return { assets: [], errors: [], skipped: [] };
|
|
59
|
+
}
|
|
60
|
+
const work = refs.map(async (ref) => {
|
|
61
|
+
const resolver = this.pick(ref);
|
|
62
|
+
if (!resolver) {
|
|
63
|
+
return { kind: "skip", ref };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const asset = await resolver.resolve(ref, ctx);
|
|
67
|
+
return { kind: "ok", asset };
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return { kind: "err", ref, error: e };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const results = await Promise.all(work);
|
|
74
|
+
const out = { assets: [], errors: [], skipped: [] };
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
if (r.kind === "ok")
|
|
77
|
+
out.assets.push(r.asset);
|
|
78
|
+
else if (r.kind === "err")
|
|
79
|
+
out.errors.push({ ref: r.ref, error: r.error });
|
|
80
|
+
else
|
|
81
|
+
out.skipped.push(r.ref);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
pick(ref) {
|
|
86
|
+
for (const r of this.resolvers) {
|
|
87
|
+
if (r.match(ref))
|
|
88
|
+
return r;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function stableKey(ref) {
|
|
94
|
+
switch (ref.kind) {
|
|
95
|
+
case "url":
|
|
96
|
+
return `url:${ref.url}`;
|
|
97
|
+
case "file":
|
|
98
|
+
return `file:${ref.path}`;
|
|
99
|
+
case "text":
|
|
100
|
+
return `text:${ref.text}`;
|
|
101
|
+
case "coordinate":
|
|
102
|
+
return `coord:${ref.lat},${ref.lon}`;
|
|
103
|
+
case "beeos-file":
|
|
104
|
+
return `beeos-file:${ref.fileId}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/resources/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BeeOS File Resolver — turn a `beeos-file://<fileId>` ref into a
|
|
3
|
+
* device-local asset.
|
|
4
|
+
*
|
|
5
|
+
* The web UI emits an ACP `resource_link` block with
|
|
6
|
+
* `uri: "beeos-file://<fileId>"` for every file the user uploads via
|
|
7
|
+
* the chat input. The fileId addresses an opaque blob in the platform's
|
|
8
|
+
* file storage (S3 / MinIO). To get the bytes onto the device this
|
|
9
|
+
* resolver:
|
|
10
|
+
*
|
|
11
|
+
* 1. Calls the Agent Gateway's `GET /api/v1/agent/files/{id}/resolve`
|
|
12
|
+
* (Ed25519-authenticated) which returns a short-lived presigned
|
|
13
|
+
* download URL plus mime/name/size metadata.
|
|
14
|
+
* 2. Constructs a synthetic `kind: "url"` ref pointing at the
|
|
15
|
+
* presigned URL and delegates to the supplied `UrlResolver`. The
|
|
16
|
+
* url resolver does the heavy lifting (streaming download, MIME
|
|
17
|
+
* whitelist, content-length cap, push via MCP fileWrite, Android
|
|
18
|
+
* MediaScanner broadcast).
|
|
19
|
+
* 3. Rewrites the returned `ResolvedAsset` so the public fields refer
|
|
20
|
+
* to the original `beeos-file://<fileId>` form. Most importantly,
|
|
21
|
+
* the SigV4-signed presigned URL is NEVER copied into `sourceUrl`
|
|
22
|
+
* or any logged field — it would leak into LLM context (via the
|
|
23
|
+
* augmenter) and structured logs.
|
|
24
|
+
*
|
|
25
|
+
* The presigned URL points at the platform's object store. In production
|
|
26
|
+
* that's a public S3/MinIO host; in local dev it's `localhost:9000`.
|
|
27
|
+
* SSRF defenses (`denyPrivateIp`) MUST therefore be turned off for the
|
|
28
|
+
* delegated fetch — the resolve endpoint is already an authenticated,
|
|
29
|
+
* server-vended URL, so the loopback target is by-design and not a
|
|
30
|
+
* vector. MIME / size / cancellation defenses still apply.
|
|
31
|
+
*/
|
|
32
|
+
import type { ResolvedAsset, ResolverContext, ResourceRef, ResourceResolver } from "../types.js";
|
|
33
|
+
import { type UrlResolver } from "./url-resolver.js";
|
|
34
|
+
import { type AgentGatewayIdentity, type ResolvedAgentFile } from "../../adapters/agent-gateway-client.js";
|
|
35
|
+
export interface BeeosFileResolverOptions {
|
|
36
|
+
/** Agent Gateway base URL, e.g. `http://localhost:8083` or `https://agent-gw.beeos.ai`. */
|
|
37
|
+
agentGatewayUrl: string;
|
|
38
|
+
/** Ed25519 identity used to sign the resolve request. */
|
|
39
|
+
identity: AgentGatewayIdentity;
|
|
40
|
+
/** UrlResolver instance to delegate the actual download/push pipeline to. */
|
|
41
|
+
urlResolver: UrlResolver;
|
|
42
|
+
/** Test seam — Agent Gateway resolve endpoint timeout. Default 10s. */
|
|
43
|
+
resolveTimeoutMs?: number;
|
|
44
|
+
/** Test seam — replaces `resolveAgentFile` for unit tests. */
|
|
45
|
+
resolveImpl?: (args: {
|
|
46
|
+
agentGatewayUrl: string;
|
|
47
|
+
identity: AgentGatewayIdentity;
|
|
48
|
+
fileId: string;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}) => Promise<ResolvedAgentFile>;
|
|
51
|
+
}
|
|
52
|
+
export declare class BeeosFileResolver implements ResourceResolver {
|
|
53
|
+
readonly name = "beeos-file";
|
|
54
|
+
private readonly agentGatewayUrl;
|
|
55
|
+
private readonly identity;
|
|
56
|
+
private readonly urlResolver;
|
|
57
|
+
private readonly resolveTimeoutMs;
|
|
58
|
+
private readonly resolveImpl;
|
|
59
|
+
constructor(opts: BeeosFileResolverOptions);
|
|
60
|
+
match(ref: ResourceRef): boolean;
|
|
61
|
+
resolve(ref: ResourceRef, ctx: ResolverContext): Promise<ResolvedAsset>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BeeOS File Resolver — turn a `beeos-file://<fileId>` ref into a
|
|
3
|
+
* device-local asset.
|
|
4
|
+
*
|
|
5
|
+
* The web UI emits an ACP `resource_link` block with
|
|
6
|
+
* `uri: "beeos-file://<fileId>"` for every file the user uploads via
|
|
7
|
+
* the chat input. The fileId addresses an opaque blob in the platform's
|
|
8
|
+
* file storage (S3 / MinIO). To get the bytes onto the device this
|
|
9
|
+
* resolver:
|
|
10
|
+
*
|
|
11
|
+
* 1. Calls the Agent Gateway's `GET /api/v1/agent/files/{id}/resolve`
|
|
12
|
+
* (Ed25519-authenticated) which returns a short-lived presigned
|
|
13
|
+
* download URL plus mime/name/size metadata.
|
|
14
|
+
* 2. Constructs a synthetic `kind: "url"` ref pointing at the
|
|
15
|
+
* presigned URL and delegates to the supplied `UrlResolver`. The
|
|
16
|
+
* url resolver does the heavy lifting (streaming download, MIME
|
|
17
|
+
* whitelist, content-length cap, push via MCP fileWrite, Android
|
|
18
|
+
* MediaScanner broadcast).
|
|
19
|
+
* 3. Rewrites the returned `ResolvedAsset` so the public fields refer
|
|
20
|
+
* to the original `beeos-file://<fileId>` form. Most importantly,
|
|
21
|
+
* the SigV4-signed presigned URL is NEVER copied into `sourceUrl`
|
|
22
|
+
* or any logged field — it would leak into LLM context (via the
|
|
23
|
+
* augmenter) and structured logs.
|
|
24
|
+
*
|
|
25
|
+
* The presigned URL points at the platform's object store. In production
|
|
26
|
+
* that's a public S3/MinIO host; in local dev it's `localhost:9000`.
|
|
27
|
+
* SSRF defenses (`denyPrivateIp`) MUST therefore be turned off for the
|
|
28
|
+
* delegated fetch — the resolve endpoint is already an authenticated,
|
|
29
|
+
* server-vended URL, so the loopback target is by-design and not a
|
|
30
|
+
* vector. MIME / size / cancellation defenses still apply.
|
|
31
|
+
*/
|
|
32
|
+
import { ResolverError } from "./url-resolver.js";
|
|
33
|
+
import { resolveAgentFile, } from "../../adapters/agent-gateway-client.js";
|
|
34
|
+
export class BeeosFileResolver {
|
|
35
|
+
name = "beeos-file";
|
|
36
|
+
agentGatewayUrl;
|
|
37
|
+
identity;
|
|
38
|
+
urlResolver;
|
|
39
|
+
resolveTimeoutMs;
|
|
40
|
+
resolveImpl;
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this.agentGatewayUrl = opts.agentGatewayUrl;
|
|
43
|
+
this.identity = opts.identity;
|
|
44
|
+
this.urlResolver = opts.urlResolver;
|
|
45
|
+
this.resolveTimeoutMs = opts.resolveTimeoutMs ?? 10_000;
|
|
46
|
+
this.resolveImpl = opts.resolveImpl ?? resolveAgentFile;
|
|
47
|
+
}
|
|
48
|
+
match(ref) {
|
|
49
|
+
return ref.kind === "beeos-file";
|
|
50
|
+
}
|
|
51
|
+
async resolve(ref, ctx) {
|
|
52
|
+
if (ref.kind !== "beeos-file") {
|
|
53
|
+
throw new ResolverError("fetch_error", `BeeosFileResolver: unsupported ref kind ${ref.kind}`);
|
|
54
|
+
}
|
|
55
|
+
if (ctx.abortSignal.aborted) {
|
|
56
|
+
throw new ResolverError("aborted", "resolve aborted before start");
|
|
57
|
+
}
|
|
58
|
+
let presigned;
|
|
59
|
+
try {
|
|
60
|
+
presigned = await this.resolveImpl({
|
|
61
|
+
agentGatewayUrl: this.agentGatewayUrl,
|
|
62
|
+
identity: this.identity,
|
|
63
|
+
fileId: ref.fileId,
|
|
64
|
+
timeoutMs: this.resolveTimeoutMs,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new ResolverError("fetch_error", `agent-gateway resolve failed for ${ref.fileId}: ${e.message}`, { fileId: ref.fileId });
|
|
69
|
+
}
|
|
70
|
+
if (!presigned.downloadUrl) {
|
|
71
|
+
throw new ResolverError("fetch_error", `agent-gateway returned empty downloadUrl for ${ref.fileId}`, { fileId: ref.fileId });
|
|
72
|
+
}
|
|
73
|
+
// The presigned URL points at the platform's object store
|
|
74
|
+
// (S3/MinIO). It's a server-vended URL we already trust because
|
|
75
|
+
// the resolve endpoint is Ed25519-authenticated; loopback in dev
|
|
76
|
+
// is by-design, so we turn off `denyPrivateIp` for the delegated
|
|
77
|
+
// fetch. MIME / size / cancellation defenses are unchanged.
|
|
78
|
+
//
|
|
79
|
+
// We also rewrite progress events from the inner UrlResolver so
|
|
80
|
+
// they carry the OUTER `beeos-file` ref shape. UrlResolver emits
|
|
81
|
+
// `phase: start/update/end` events with `ref: { kind: "url",
|
|
82
|
+
// url: <presigned> }` — those events flow into the AgentHost's
|
|
83
|
+
// `task:tool` stream and ultimately into the chat-UI tool card,
|
|
84
|
+
// which would leak the SigV4 token. Rewriting the ref keeps the
|
|
85
|
+
// progress UX (downloaded/total bytes, success/failure) intact
|
|
86
|
+
// while showing only the opaque `beeos-file://<fileId>` identity.
|
|
87
|
+
const outerEmit = ctx.emitProgress;
|
|
88
|
+
const innerCtx = {
|
|
89
|
+
...ctx,
|
|
90
|
+
denyPrivateIp: false,
|
|
91
|
+
emitProgress: outerEmit
|
|
92
|
+
? (ev) => outerEmit({ ...ev, ref })
|
|
93
|
+
: undefined,
|
|
94
|
+
};
|
|
95
|
+
const innerRef = { kind: "url", url: presigned.downloadUrl };
|
|
96
|
+
const inner = await this.urlResolver.resolve(innerRef, innerCtx);
|
|
97
|
+
// Strip the SigV4 token — the presigned URL must NEVER reach
|
|
98
|
+
// augmenter / logs / LLM context. Replace `ref` and `sourceUrl`
|
|
99
|
+
// with the opaque `beeos-file://<fileId>` form so downstream
|
|
100
|
+
// consumers can still correlate (telemetry, augmenter rendering)
|
|
101
|
+
// without exposing the presigned URL.
|
|
102
|
+
return {
|
|
103
|
+
...inner,
|
|
104
|
+
ref,
|
|
105
|
+
sourceUrl: `beeos-file://${ref.fileId}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=beeos-file-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beeos-file-resolver.js","sourceRoot":"","sources":["../../../src/resources/resolvers/beeos-file-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver error vocabulary — kept as a closed enum so Prometheus
|
|
3
|
+
* counters never leak high-cardinality strings, and so the AgentHost
|
|
4
|
+
* can dispatch on a finite set of structured failure modes.
|
|
5
|
+
*
|
|
6
|
+
* The labels here align 1:1 with `ResolveOutcomeLabel` in `types.ts`;
|
|
7
|
+
* this module is the source of truth for the *error* subset (and `success`
|
|
8
|
+
* lives at the call site as a sentinel — never thrown).
|
|
9
|
+
*/
|
|
10
|
+
export type ResolverErrorCode = "fetch_error" | "push_error" | "scanner_failed" | "disallowed_scheme" | "oversized" | "timeout" | "private_ip_blocked" | "dns_rebind_blocked" | "disallowed_mime" | "disk_pressure" | "aborted";
|
|
11
|
+
export declare const AGENT_HOST_ERROR_LABELS: Record<ResolverErrorCode, string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver error vocabulary — kept as a closed enum so Prometheus
|
|
3
|
+
* counters never leak high-cardinality strings, and so the AgentHost
|
|
4
|
+
* can dispatch on a finite set of structured failure modes.
|
|
5
|
+
*
|
|
6
|
+
* The labels here align 1:1 with `ResolveOutcomeLabel` in `types.ts`;
|
|
7
|
+
* this module is the source of truth for the *error* subset (and `success`
|
|
8
|
+
* lives at the call site as a sentinel — never thrown).
|
|
9
|
+
*/
|
|
10
|
+
export const AGENT_HOST_ERROR_LABELS = {
|
|
11
|
+
fetch_error: "fetch_error",
|
|
12
|
+
push_error: "push_error",
|
|
13
|
+
scanner_failed: "scanner_failed",
|
|
14
|
+
disallowed_scheme: "disallowed_scheme",
|
|
15
|
+
oversized: "oversized",
|
|
16
|
+
timeout: "timeout",
|
|
17
|
+
private_ip_blocked: "private_ip_blocked",
|
|
18
|
+
dns_rebind_blocked: "dns_rebind_blocked",
|
|
19
|
+
disallowed_mime: "disallowed_mime",
|
|
20
|
+
disk_pressure: "disk_pressure",
|
|
21
|
+
aborted: "aborted",
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/resources/resolvers/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,CAAC,MAAM,uBAAuB,GAAsC;IACxE,WAAW,EAAE,aAAa;IAC1B,UAAU,EAAE,YAAY;IACxB,cAAc,EAAE,gBAAgB;IAChC,iBAAiB,EAAE,mBAAmB;IACtC,SAAS,EAAE,WAAW;IACtB,OAAO,EAAE,SAAS;IAClB,kBAAkB,EAAE,oBAAoB;IACxC,kBAAkB,EAAE,oBAAoB;IACxC,eAAe,EAAE,iBAAiB;IAClC,aAAa,EAAE,eAAe;IAC9B,OAAO,EAAE,SAAS;CACnB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME → file-extension whitelist.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: this module is the SOLE source of file extensions used by
|
|
5
|
+
* the URL resolver. Extensions are NEVER derived from URL paths —
|
|
6
|
+
* doing so opens a path-traversal / extension-injection vector when
|
|
7
|
+
* the upstream serves a `.html` URL with `Content-Type: image/jpeg`.
|
|
8
|
+
*
|
|
9
|
+
* Two surfaces:
|
|
10
|
+
* - `ALLOWED_MIMES` — exact MIME strings we accept.
|
|
11
|
+
* - `ALLOWED_MIME_PREFIXES` — type/subtype prefixes (`image/`,
|
|
12
|
+
* `video/`, `audio/`) for which we accept any subtype on the
|
|
13
|
+
* condition that the resulting `mapMimeToExt` returns a real ext.
|
|
14
|
+
*
|
|
15
|
+
* A MIME is "allowed" iff:
|
|
16
|
+
* 1. It appears in `ALLOWED_MIMES` OR matches a prefix in
|
|
17
|
+
* `ALLOWED_MIME_PREFIXES`, AND
|
|
18
|
+
* 2. `mapMimeToExt` returns a non-null extension.
|
|
19
|
+
*
|
|
20
|
+
* Add new entries by extending BOTH `ALLOWED_MIMES` (or appending to
|
|
21
|
+
* `ALLOWED_MIME_PREFIXES`) AND `MIME_TO_EXT`. Adding only one side is
|
|
22
|
+
* a bug — the resolver will accept the MIME but reject the resource
|
|
23
|
+
* for not having a whitelisted extension, leading to confusing errors.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Strict whitelist — every entry maps to exactly one extension. Adding
|
|
27
|
+
* a new entry requires explicit security review (no dynamic
|
|
28
|
+
* extensions, no falling back to URL-path extension parsing).
|
|
29
|
+
*/
|
|
30
|
+
export declare const MIME_TO_EXT: Record<string, string>;
|
|
31
|
+
/** Exact-match MIMEs (a strict subset of `MIME_TO_EXT` keys). */
|
|
32
|
+
export declare const ALLOWED_MIMES: ReadonlySet<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Prefix-match MIMEs. The resolver still rejects the request if the
|
|
35
|
+
* `mapMimeToExt` lookup returns null, so adding a prefix here is safe.
|
|
36
|
+
*
|
|
37
|
+
* `application/*` is intentionally NOT a prefix — we want strict allow-
|
|
38
|
+
* listing for application subtypes (executables, installers, archives).
|
|
39
|
+
*/
|
|
40
|
+
export declare const ALLOWED_MIME_PREFIXES: readonly string[];
|
|
41
|
+
export declare function mapMimeToExt(mime: string): string | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME → file-extension whitelist.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: this module is the SOLE source of file extensions used by
|
|
5
|
+
* the URL resolver. Extensions are NEVER derived from URL paths —
|
|
6
|
+
* doing so opens a path-traversal / extension-injection vector when
|
|
7
|
+
* the upstream serves a `.html` URL with `Content-Type: image/jpeg`.
|
|
8
|
+
*
|
|
9
|
+
* Two surfaces:
|
|
10
|
+
* - `ALLOWED_MIMES` — exact MIME strings we accept.
|
|
11
|
+
* - `ALLOWED_MIME_PREFIXES` — type/subtype prefixes (`image/`,
|
|
12
|
+
* `video/`, `audio/`) for which we accept any subtype on the
|
|
13
|
+
* condition that the resulting `mapMimeToExt` returns a real ext.
|
|
14
|
+
*
|
|
15
|
+
* A MIME is "allowed" iff:
|
|
16
|
+
* 1. It appears in `ALLOWED_MIMES` OR matches a prefix in
|
|
17
|
+
* `ALLOWED_MIME_PREFIXES`, AND
|
|
18
|
+
* 2. `mapMimeToExt` returns a non-null extension.
|
|
19
|
+
*
|
|
20
|
+
* Add new entries by extending BOTH `ALLOWED_MIMES` (or appending to
|
|
21
|
+
* `ALLOWED_MIME_PREFIXES`) AND `MIME_TO_EXT`. Adding only one side is
|
|
22
|
+
* a bug — the resolver will accept the MIME but reject the resource
|
|
23
|
+
* for not having a whitelisted extension, leading to confusing errors.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Strict whitelist — every entry maps to exactly one extension. Adding
|
|
27
|
+
* a new entry requires explicit security review (no dynamic
|
|
28
|
+
* extensions, no falling back to URL-path extension parsing).
|
|
29
|
+
*/
|
|
30
|
+
export const MIME_TO_EXT = {
|
|
31
|
+
// images
|
|
32
|
+
"image/jpeg": "jpg",
|
|
33
|
+
"image/jpg": "jpg",
|
|
34
|
+
"image/png": "png",
|
|
35
|
+
"image/gif": "gif",
|
|
36
|
+
"image/webp": "webp",
|
|
37
|
+
"image/heic": "heic",
|
|
38
|
+
"image/heif": "heif",
|
|
39
|
+
"image/avif": "avif",
|
|
40
|
+
"image/svg+xml": "svg",
|
|
41
|
+
"image/bmp": "bmp",
|
|
42
|
+
// video
|
|
43
|
+
"video/mp4": "mp4",
|
|
44
|
+
"video/quicktime": "mov",
|
|
45
|
+
"video/webm": "webm",
|
|
46
|
+
"video/3gpp": "3gp",
|
|
47
|
+
"video/x-matroska": "mkv",
|
|
48
|
+
// audio
|
|
49
|
+
"audio/mpeg": "mp3",
|
|
50
|
+
"audio/mp4": "m4a",
|
|
51
|
+
"audio/aac": "aac",
|
|
52
|
+
"audio/wav": "wav",
|
|
53
|
+
"audio/x-wav": "wav",
|
|
54
|
+
"audio/ogg": "ogg",
|
|
55
|
+
"audio/webm": "weba",
|
|
56
|
+
"audio/flac": "flac",
|
|
57
|
+
// documents
|
|
58
|
+
"application/pdf": "pdf",
|
|
59
|
+
"application/zip": "zip",
|
|
60
|
+
"application/vnd.android.package-archive": "apk",
|
|
61
|
+
"application/msword": "doc",
|
|
62
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
63
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
64
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
65
|
+
"application/vnd.ms-excel": "xls",
|
|
66
|
+
"application/vnd.ms-powerpoint": "ppt",
|
|
67
|
+
// text
|
|
68
|
+
"text/plain": "txt",
|
|
69
|
+
"text/csv": "csv",
|
|
70
|
+
};
|
|
71
|
+
/** Exact-match MIMEs (a strict subset of `MIME_TO_EXT` keys). */
|
|
72
|
+
export const ALLOWED_MIMES = new Set(Object.keys(MIME_TO_EXT));
|
|
73
|
+
/**
|
|
74
|
+
* Prefix-match MIMEs. The resolver still rejects the request if the
|
|
75
|
+
* `mapMimeToExt` lookup returns null, so adding a prefix here is safe.
|
|
76
|
+
*
|
|
77
|
+
* `application/*` is intentionally NOT a prefix — we want strict allow-
|
|
78
|
+
* listing for application subtypes (executables, installers, archives).
|
|
79
|
+
*/
|
|
80
|
+
export const ALLOWED_MIME_PREFIXES = [
|
|
81
|
+
"image/",
|
|
82
|
+
"video/",
|
|
83
|
+
"audio/",
|
|
84
|
+
];
|
|
85
|
+
export function mapMimeToExt(mime) {
|
|
86
|
+
const lower = mime.toLowerCase();
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(MIME_TO_EXT, lower)) {
|
|
88
|
+
return MIME_TO_EXT[lower];
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=mime-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime-table.js","sourceRoot":"","sources":["../../../src/resources/resolvers/mime-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD,SAAS;IACT,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,eAAe,EAAE,KAAK;IACtB,WAAW,EAAE,KAAK;IAElB,QAAQ;IACR,WAAW,EAAE,KAAK;IAClB,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,KAAK;IACnB,kBAAkB,EAAE,KAAK;IAEzB,QAAQ;IACR,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IAEpB,YAAY;IACZ,iBAAiB,EAAE,KAAK;IACxB,iBAAiB,EAAE,KAAK;IACxB,yCAAyC,EAAE,KAAK;IAChD,oBAAoB,EAAE,KAAK;IAC3B,yEAAyE,EAAE,MAAM;IACjF,mEAAmE,EAAE,MAAM;IAC3E,2EAA2E,EAAE,MAAM;IACnF,0BAA0B,EAAE,KAAK;IACjC,+BAA+B,EAAE,KAAK;IAEtC,OAAO;IACP,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,KAAK;CAClB,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;AAEpF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,QAAQ;IACR,QAAQ;IACR,QAAQ;CACT,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;QAC7D,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Resolver — fetch HTTP(S) URLs and push them onto the device.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 / Pillar 1 of the GUI-Shortcut Hybrid Plan.
|
|
5
|
+
*
|
|
6
|
+
* Pipeline:
|
|
7
|
+
*
|
|
8
|
+
* 1. validate scheme — `http` / `https` only (allowedSchemes).
|
|
9
|
+
* 2. SSRF check — DNS resolve the hostname; reject any private
|
|
10
|
+
* / loopback / link-local / multicast IP.
|
|
11
|
+
* 3. fetch (streaming) — chunk-by-chunk with running size cap and
|
|
12
|
+
* AbortSignal merge. Validates Content-Type
|
|
13
|
+
* against the platform-supported MIME table.
|
|
14
|
+
* 4. DNS-rebind check — after the body has been read, re-resolve the
|
|
15
|
+
* hostname and assert at least one returned
|
|
16
|
+
* IP overlaps with the IP we accepted in (2).
|
|
17
|
+
* 5. choose ext — strictly via Content-Type → ext mapping.
|
|
18
|
+
* Path-derived extensions are NEVER trusted.
|
|
19
|
+
* 6. choose target — `ctx.device.resolveWellKnownDir(mime)` picks
|
|
20
|
+
* the platform-appropriate directory.
|
|
21
|
+
* 7. push — `ctx.mcp.fileWrite(remotePath, bytes)`.
|
|
22
|
+
* 8. media-scan — Android only: `touch -d now <path> &&
|
|
23
|
+
* am broadcast -a MEDIA_SCANNER_SCAN_FILE`
|
|
24
|
+
* so Gallery / WeChat / Photos see it as
|
|
25
|
+
* fresh and orderable. Best-effort; failure
|
|
26
|
+
* does NOT fail the resolve.
|
|
27
|
+
*
|
|
28
|
+
* All four SSRF defenses operate on the same `ResolverContext` so the
|
|
29
|
+
* AgentHost can flip them off in tests / lab environments via the
|
|
30
|
+
* feature flag.
|
|
31
|
+
*/
|
|
32
|
+
import { type LookupAddress } from "node:dns";
|
|
33
|
+
import { type ResolverErrorCode } from "./errors.js";
|
|
34
|
+
import type { ResolvedAsset, ResolverContext, ResourceRef, ResourceResolver } from "../types.js";
|
|
35
|
+
export declare class ResolverError extends Error {
|
|
36
|
+
readonly code: ResolverErrorCode;
|
|
37
|
+
readonly meta?: Record<string, unknown> | undefined;
|
|
38
|
+
constructor(code: ResolverErrorCode, message: string, meta?: Record<string, unknown> | undefined);
|
|
39
|
+
/** Promtheus-friendly outcome label (subset of error codes). */
|
|
40
|
+
get outcomeLabel(): string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Optional injection hooks for tests. Production passes the real
|
|
44
|
+
* `globalThis.fetch` and `dns.lookup`.
|
|
45
|
+
*/
|
|
46
|
+
export interface UrlResolverOptions {
|
|
47
|
+
fetchImpl?: typeof fetch;
|
|
48
|
+
dnsLookup?: (hostname: string) => Promise<LookupAddress[]>;
|
|
49
|
+
/**
|
|
50
|
+
* When set, the platform OS hint is forced to this value, bypassing
|
|
51
|
+
* `ctx.device.getInfo().os` lookup. Tests use this to drive the
|
|
52
|
+
* "Android-only media-scan" branch deterministically.
|
|
53
|
+
*/
|
|
54
|
+
forceDeviceType?: string;
|
|
55
|
+
}
|
|
56
|
+
export declare class UrlResolver implements ResourceResolver {
|
|
57
|
+
readonly name = "url";
|
|
58
|
+
private readonly fetchImpl;
|
|
59
|
+
private readonly dnsLookup;
|
|
60
|
+
private readonly forceDeviceType?;
|
|
61
|
+
constructor(opts?: UrlResolverOptions);
|
|
62
|
+
match(ref: ResourceRef): boolean;
|
|
63
|
+
resolve(ref: ResourceRef, ctx: ResolverContext): Promise<ResolvedAsset>;
|
|
64
|
+
private doResolve;
|
|
65
|
+
private detectPlatform;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* RFC 1918 / RFC 4193 / loopback / link-local / multicast / etc check.
|
|
69
|
+
* We deliberately keep this exhaustive rather than rely on a third-party
|
|
70
|
+
* package — the SSRF surface is small and security-critical.
|
|
71
|
+
*
|
|
72
|
+
* IPv4 loopback — 127.0.0.0/8
|
|
73
|
+
* IPv4 RFC1918 private — 10/8, 172.16/12, 192.168/16
|
|
74
|
+
* IPv4 link-local — 169.254/16
|
|
75
|
+
* IPv4 CGNAT — 100.64/10
|
|
76
|
+
* IPv4 broadcast / multicast — 224/4, 255.255.255.255
|
|
77
|
+
* IPv4 0.0.0.0/8 — "this network"
|
|
78
|
+
* IPv6 loopback — ::1
|
|
79
|
+
* IPv6 link-local — fe80::/10
|
|
80
|
+
* IPv6 ULA — fc00::/7
|
|
81
|
+
* IPv6 multicast — ff00::/8
|
|
82
|
+
* IPv6 unspecified — ::
|
|
83
|
+
* IPv4-mapped IPv6 — ::ffff:0:0/96 (mapped to v4 above)
|
|
84
|
+
*/
|
|
85
|
+
export declare function isPrivateOrLoopback(addr: string, family: number): boolean;
|