@jini-ai/platform 0.5.8 → 0.5.10
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/CHANGELOG.md +12 -0
- package/dist/asset-cache.js +26 -25
- package/dist/blob-storage.d.ts +7 -2
- package/dist/blob-storage.js +99 -59
- package/dist/child-environment.d.ts +36 -0
- package/dist/child-environment.js +68 -0
- package/dist/download.js +192 -148
- package/dist/fs/atomic-write.d.ts +25 -2
- package/dist/fs/atomic-write.js +162 -111
- package/dist/fs/boot-identity.d.ts +16 -0
- package/dist/fs/boot-identity.js +38 -17
- package/dist/fs/directory-home.js +6 -0
- package/dist/fs/directory-registry.d.ts +1 -1
- package/dist/fs/directory-registry.js +23 -16
- package/dist/fs/directory-running-lock.d.ts +1 -1
- package/dist/fs/directory-running-lock.js +18 -7
- package/dist/fs/directory-sync.d.ts +1 -1
- package/dist/fs/directory-sync.js +64 -51
- package/dist/fs/directory-user-config.js +108 -67
- package/dist/fs/directory-workspace.d.ts +11 -0
- package/dist/fs/directory-workspace.js +16 -11
- package/dist/fs/durable-json.d.ts +3 -1
- package/dist/fs/durable-json.js +55 -40
- package/dist/fs/file-lock.js +214 -149
- package/dist/fs/guarded-reader.js +47 -35
- package/dist/fs/user-data-directories.d.ts +1 -1
- package/dist/fs/user-data-directories.js +25 -9
- package/dist/fs/verified-directory-move.d.ts +14 -11
- package/dist/fs/verified-directory-move.js +106 -80
- package/dist/fs/verified-file-relocation.d.ts +2 -1
- package/dist/fs/verified-file-relocation.js +112 -100
- package/dist/http/guarded/client.d.ts +3 -4
- package/dist/http/guarded/client.js +50 -60
- package/dist/http/guarded/fetch-adapter.d.ts +2 -3
- package/dist/http/guarded/fetch-adapter.js +26 -15
- package/dist/mail/builtin-providers.d.ts +39 -0
- package/dist/mail/builtin-providers.js +60 -0
- package/dist/mail/feedback.d.ts +84 -0
- package/dist/mail/feedback.js +90 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.js +6 -0
- package/dist/mail/policy.d.ts +77 -0
- package/dist/mail/policy.js +155 -0
- package/dist/mail/ports.d.ts +56 -7
- package/dist/mail/provider.d.ts +117 -0
- package/dist/mail/provider.js +74 -0
- package/dist/mail/rate-limit.d.ts +33 -0
- package/dist/mail/rate-limit.js +35 -0
- package/dist/mail/sender-identity.d.ts +68 -0
- package/dist/mail/sender-identity.js +50 -0
- package/dist/mail/types.d.ts +9 -0
- package/dist/net/address.d.ts +9 -23
- package/dist/net/address.js +28 -133
- package/dist/net/endpoint-policy.d.ts +26 -2
- package/dist/net/endpoint-policy.js +159 -56
- package/dist/object-store/index.d.ts +2 -0
- package/dist/object-store/index.js +2 -0
- package/dist/object-store/local-fs.d.ts +17 -0
- package/dist/object-store/local-fs.js +146 -0
- package/dist/process.js +2 -0
- package/dist/proxy-env.js +131 -135
- package/dist/secrets/credential-sets/contracts.d.ts +1 -0
- package/dist/secrets/credential-sets/contracts.js +1 -0
- package/dist/secrets/credential-sets/index.d.ts +1 -0
- package/dist/secrets/credential-sets/index.js +1 -0
- package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
- package/dist/secrets/credential-sets/provider-matches.js +61 -0
- package/dist/secrets/credential-sets/types.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.meta.js +58 -20
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
- package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
- package/dist/secrets/credential-sets/vault.service.js +131 -61
- package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
- package/dist/secrets/credential-sets/vault.types.js +22 -8
- package/dist/secrets/keyring.env.js +14 -10
- package/dist/toolchain.js +119 -91
- package/dist/web/html-document.js +73 -69
- package/dist/web/html-to-markdown.js +127 -102
- package/dist/web/web-page.d.ts +1 -1
- package/dist/web/web-page.js +162 -81
- package/package.json +11 -3
package/dist/net/address.d.ts
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
|
|
11
|
-
* returning 6 — empirically re-verified this session by fuzzing ~10k
|
|
12
|
-
* `isIP`-accepted v6 literals through this function's guards with zero
|
|
13
|
-
* hits — so none of these guards are reachable through that path. They stay
|
|
14
|
-
* as real defense-in-depth (fail closed with `null` rather than trust a
|
|
15
|
-
* platform primitive never changing behavior), matching the asset cache's
|
|
16
|
-
* exported-for-direct-testing precedent (`readBodyCapped`'s abort parameter). */
|
|
17
|
-
export declare function expandIpv6({ address: addr }: {
|
|
18
|
-
address: string;
|
|
19
|
-
}): number[] | null;
|
|
20
|
-
/** True for any address an external CDN must never resolve to: loopback,
|
|
21
|
-
* link-local, private, CGNAT, ULA, multicast, or unspecified. An unparseable
|
|
22
|
-
* input is treated as unsafe. */
|
|
23
|
-
export declare function isPrivateAddress({ address: addr }: {
|
|
1
|
+
import { type AddressClass } from './endpoint-policy.js';
|
|
2
|
+
export { expandIpv6 } from './endpoint-policy.js';
|
|
3
|
+
export type { AddressClass } from './endpoint-policy.js';
|
|
4
|
+
/** Strict Node peer classification; invalid literals fail closed to reserved. */
|
|
5
|
+
export declare function classifyIpAddress({ ip }: {
|
|
6
|
+
ip: string;
|
|
7
|
+
}, _options?: Record<string, never>): AddressClass;
|
|
8
|
+
/** True for loopback, link-local, private, CGNAT, ULA, multicast or unspecified; invalid input fails closed. */
|
|
9
|
+
export declare function isPrivateAddress({ address }: {
|
|
24
10
|
address: string;
|
|
25
11
|
}): boolean;
|
|
26
12
|
export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
|
package/dist/net/address.js
CHANGED
|
@@ -1,139 +1,34 @@
|
|
|
1
1
|
import { isIP } from 'node:net';
|
|
2
|
+
import { classifyAddressLiteral } from './endpoint-policy.js';
|
|
2
3
|
// SSRF peer classification is distinct from core's browser-development LAN origin allow-list.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const v4 = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(s);
|
|
28
|
-
if (v4) {
|
|
29
|
-
// `v4[2]` is the regex's second (mandatory, non-optional) capture group
|
|
30
|
-
// of a successful match — always defined.
|
|
31
|
-
const o = v4[2].split('.').map((n) => Number(n));
|
|
32
|
-
if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
33
|
-
return null;
|
|
34
|
-
// `o` is now a validated 4-element array — indices 0-3 always defined.
|
|
35
|
-
const hi = (((o[0] << 8) | o[1])).toString(16);
|
|
36
|
-
const lo = (((o[2] << 8) | o[3])).toString(16);
|
|
37
|
-
s = `${v4[1]}${hi}:${lo}`;
|
|
38
|
-
}
|
|
39
|
-
const halves = s.split('::');
|
|
40
|
-
if (halves.length > 2)
|
|
41
|
-
return null;
|
|
42
|
-
const parse = (p) => p === '' ? [] : p.split(':').map((g) => (/^[0-9a-f]{1,4}$/.test(g) ? parseInt(g, 16) : Number.NaN));
|
|
43
|
-
// `halves` always has at least one element (split guarantee).
|
|
44
|
-
const left = parse(halves[0]);
|
|
45
|
-
let groups;
|
|
46
|
-
if (halves.length === 1) {
|
|
47
|
-
groups = left;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// `halves.length === 2` here (the `> 2` case already returned above).
|
|
51
|
-
const right = parse(halves[1]);
|
|
52
|
-
const fill = 8 - left.length - right.length;
|
|
53
|
-
if (fill < 1)
|
|
54
|
-
return null;
|
|
55
|
-
groups = [...left, ...new Array(fill).fill(0), ...right];
|
|
56
|
-
}
|
|
57
|
-
if (groups.length !== 8 || groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff))
|
|
58
|
-
return null;
|
|
59
|
-
return groups;
|
|
4
|
+
export { expandIpv6 } from './endpoint-policy.js';
|
|
5
|
+
/** Strict Node peer classification; invalid literals fail closed to reserved. */
|
|
6
|
+
export function classifyIpAddress({ ip }, _options = {}) {
|
|
7
|
+
// `net.isIP(ip) === 4` already guarantees `ip` is a syntactically
|
|
8
|
+
// valid dotted-quad with four 0-255 decimal octets (no leading-zero
|
|
9
|
+
// octal ambiguity, no out-of-range component) — empirically verified
|
|
10
|
+
// in the original extraction by fuzzing 500k+ candidate strings through `isIP` and
|
|
11
|
+
// this same re-derivation with zero divergences. The shared classifier therefore receives
|
|
12
|
+
// validated octets.
|
|
13
|
+
// `net.isIP(ip) === 6` already guarantees `ip` is syntactically valid
|
|
14
|
+
// IPv6 (Node validates this before classifying the family), and
|
|
15
|
+
// `expandIpv6` implements the same canonicalization Node's own parser
|
|
16
|
+
// does, so it never returns `null` for an `isIP`-accepted literal —
|
|
17
|
+
// empirically verified in the original extraction by fuzzing ~10k `isIP`-accepted
|
|
18
|
+
// v6 literals through `expandIpv6` with zero `null` results (see
|
|
19
|
+
// `expandIpv6`'s doc comment and `archived provenance ledger`'s 2026-07-22 entry).
|
|
20
|
+
// Kept as a real fail-closed guard (defense-in-depth against a future
|
|
21
|
+
// change to either parser) rather than asserted away, so it stays
|
|
22
|
+
// provably-unreachable-but-intentional instead of a forced test.
|
|
23
|
+
// Both rejections (Node's `isIP` and the shared classifier) funnel into one
|
|
24
|
+
// fail-closed default, so the `reserved` fallback is the single place an
|
|
25
|
+
// unclassifiable literal lands — exercised by every invalid input.
|
|
26
|
+
const addressClass = isIP(ip) === 0 ? null : classifyAddressLiteral({ address: ip });
|
|
27
|
+
return addressClass ?? 'reserved';
|
|
60
28
|
}
|
|
61
|
-
/** True for
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export function isPrivateAddress({ address: addr }) {
|
|
65
|
-
const fam = isIP(addr);
|
|
66
|
-
if (fam === 4) {
|
|
67
|
-
// `net.isIP(addr) === 4` already guarantees `addr` is a syntactically
|
|
68
|
-
// valid dotted-quad with four 0-255 decimal octets (no leading-zero
|
|
69
|
-
// octal ambiguity, no out-of-range component) — empirically re-verified
|
|
70
|
-
// this session by fuzzing 500k+ candidate strings through `isIP` and
|
|
71
|
-
// this same re-derivation with zero divergences. `o` is therefore always
|
|
72
|
-
// a validated 4-element array.
|
|
73
|
-
const o = addr.split('.').map((n) => Number(n));
|
|
74
|
-
const a = o[0];
|
|
75
|
-
const b = o[1];
|
|
76
|
-
if (a === 0)
|
|
77
|
-
return true; // "this" network / unspecified
|
|
78
|
-
if (a === 10)
|
|
79
|
-
return true; // private
|
|
80
|
-
if (a === 127)
|
|
81
|
-
return true; // loopback
|
|
82
|
-
if (a === 169 && b === 254)
|
|
83
|
-
return true; // link-local
|
|
84
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
85
|
-
return true; // private
|
|
86
|
-
if (a === 192 && b === 168)
|
|
87
|
-
return true; // private
|
|
88
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
89
|
-
return true; // CGNAT
|
|
90
|
-
if (a >= 224)
|
|
91
|
-
return true; // multicast + reserved
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
if (fam === 6) {
|
|
95
|
-
const groups = expandIpv6({ address: addr });
|
|
96
|
-
// `net.isIP(addr) === 6` already guarantees `addr` is syntactically valid
|
|
97
|
-
// IPv6 (Node validates this before classifying the family), and
|
|
98
|
-
// `expandIpv6` implements the same canonicalization Node's own parser
|
|
99
|
-
// does, so it never returns `null` for an `isIP`-accepted literal —
|
|
100
|
-
// empirically re-verified this session by fuzzing ~10k `isIP`-accepted
|
|
101
|
-
// v6 literals through `expandIpv6` with zero `null` results (see
|
|
102
|
-
// `expandIpv6`'s doc comment and `archived provenance ledger`'s 2026-07-22 entry).
|
|
103
|
-
// Kept as a real fail-closed guard (defense-in-depth against a future
|
|
104
|
-
// change to either parser) rather than asserted away, so it stays
|
|
105
|
-
// provably-unreachable-but-intentional instead of a forced test.
|
|
106
|
-
if (!groups)
|
|
107
|
-
return true; // can't canonicalize a "valid" v6 → refuse
|
|
108
|
-
// IPv4-mapped (`::ffff:0:0/96`) and the deprecated IPv4-compatible
|
|
109
|
-
// (`::0:0/96`, excluding :: / ::1) carry an embedded IPv4 — classify it by
|
|
110
|
-
// its real v4 range so `::ffff:7f00:1` (== 127.0.0.1) is caught.
|
|
111
|
-
const topZero = groups.slice(0, 5).every((g) => g === 0);
|
|
112
|
-
if (topZero && (groups[5] === 0xffff || groups[5] === 0)) {
|
|
113
|
-
// `groups` is a validated 8-element array once non-null — indices 6-7
|
|
114
|
-
// always defined.
|
|
115
|
-
const a = groups[6] >> 8;
|
|
116
|
-
const b = groups[6] & 0xff;
|
|
117
|
-
const c = groups[7] >> 8;
|
|
118
|
-
const d = groups[7] & 0xff;
|
|
119
|
-
if (groups[5] === 0xffff || a !== 0 || b !== 0) {
|
|
120
|
-
return isPrivateAddress({ address: `${a}.${b}.${c}.${d}` });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (groups.every((g) => g === 0))
|
|
124
|
-
return true; // :: (unspecified)
|
|
125
|
-
if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1)
|
|
126
|
-
return true; // ::1 loopback
|
|
127
|
-
const first = groups[0];
|
|
128
|
-
if ((first & 0xffc0) === 0xfe80)
|
|
129
|
-
return true; // link-local fe80::/10 (fe80–febf)
|
|
130
|
-
if ((first & 0xfe00) === 0xfc00)
|
|
131
|
-
return true; // unique-local fc00::/7
|
|
132
|
-
if ((first & 0xff00) === 0xff00)
|
|
133
|
-
return true; // multicast ff00::/8
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return true; // not a literal IP — caller must resolve before trusting it
|
|
29
|
+
/** True for loopback, link-local, private, CGNAT, ULA, multicast or unspecified; invalid input fails closed. */
|
|
30
|
+
export function isPrivateAddress({ address }) {
|
|
31
|
+
return classifyIpAddress({ ip: address }) !== 'public';
|
|
137
32
|
}
|
|
138
33
|
// Preserve the Node entry's published names while sharing the literal-host owner with browser consumers.
|
|
139
34
|
export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Expand an IPv6 literal (with `::` compression and/or an embedded dotted
|
|
2
|
+
* IPv4 tail) into its eight 16-bit groups, or null if it does not parse.
|
|
3
|
+
* Node's URL parser normalizes mapped literals to hex (`::ffff:127.0.0.1` →
|
|
4
|
+
* `::ffff:7f00:1`), so a string regex can't reliably spot the mapped range —
|
|
5
|
+
* we have to canonicalize.
|
|
6
|
+
*
|
|
7
|
+
* Exported (previously module-private) so its parse-failure guards are
|
|
8
|
+
* directly unit-testable: the IP classifier and guarded client gate
|
|
9
|
+
* calls behind `net.isIP(addr) === 6`, and Node's `isIP` already fully
|
|
10
|
+
* validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
|
|
11
|
+
* returning 6 — empirically verified in the original extraction by fuzzing ~10k
|
|
12
|
+
* `isIP`-accepted v6 literals through this function's guards with zero
|
|
13
|
+
* hits — so none of these guards are reachable through that path. They stay
|
|
14
|
+
* as real defense-in-depth (fail closed with `null` rather than trust a
|
|
15
|
+
* platform primitive never changing behavior), matching the asset cache's
|
|
16
|
+
* exported-for-direct-testing precedent (`readBodyCapped`'s abort parameter). */
|
|
17
|
+
export declare function expandIpv6({ address: addr }: {
|
|
18
|
+
address: string;
|
|
19
|
+
}): number[] | null;
|
|
20
|
+
export type AddressClass = 'public' | 'private' | 'loopback' | 'link-local' | 'reserved';
|
|
21
|
+
/** Pure literal classifier; null means malformed input or a hostname requiring DNS. */
|
|
22
|
+
export declare function classifyAddressLiteral({ address }: {
|
|
23
|
+
address: string;
|
|
24
|
+
}, _options?: Record<string, never>): AddressClass | null;
|
|
25
|
+
/** True for localhost and literal loopback, including embedded IPv4 forms. */
|
|
2
26
|
export declare function isLoopbackApiHost({ hostname }: {
|
|
3
27
|
hostname: string;
|
|
4
28
|
}): boolean;
|
|
5
|
-
/**
|
|
29
|
+
/** Private/reserved literal hosts are blocked; configured local servers keep their loopback exception. */
|
|
6
30
|
export declare function isBlockedExternalApiHostname({ hostname }: {
|
|
7
31
|
hostname: string;
|
|
8
32
|
}): boolean;
|
|
@@ -12,7 +12,7 @@ function normalizeBracketedIpv6(hostname) {
|
|
|
12
12
|
// FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
|
|
13
13
|
// form, so `localhost.` must normalize to `localhost` before the equality
|
|
14
14
|
// check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
|
|
15
|
-
//
|
|
15
|
+
// the IPv4 classifier parses them. Strips one or more trailing dots.
|
|
16
16
|
return stripped.toLowerCase().replace(/\.+$/, '');
|
|
17
17
|
}
|
|
18
18
|
function parseIpv4(hostname) {
|
|
@@ -29,70 +29,173 @@ function parseIpv4(hostname) {
|
|
|
29
29
|
return null;
|
|
30
30
|
return [parsed[0], parsed[1], parsed[2], parsed[3]];
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/** Expand an IPv6 literal (with `::` compression and/or an embedded dotted
|
|
33
|
+
* IPv4 tail) into its eight 16-bit groups, or null if it does not parse.
|
|
34
|
+
* Node's URL parser normalizes mapped literals to hex (`::ffff:127.0.0.1` →
|
|
35
|
+
* `::ffff:7f00:1`), so a string regex can't reliably spot the mapped range —
|
|
36
|
+
* we have to canonicalize.
|
|
37
|
+
*
|
|
38
|
+
* Exported (previously module-private) so its parse-failure guards are
|
|
39
|
+
* directly unit-testable: the IP classifier and guarded client gate
|
|
40
|
+
* calls behind `net.isIP(addr) === 6`, and Node's `isIP` already fully
|
|
41
|
+
* validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
|
|
42
|
+
* returning 6 — empirically verified in the original extraction by fuzzing ~10k
|
|
43
|
+
* `isIP`-accepted v6 literals through this function's guards with zero
|
|
44
|
+
* hits — so none of these guards are reachable through that path. They stay
|
|
45
|
+
* as real defense-in-depth (fail closed with `null` rather than trust a
|
|
46
|
+
* platform primitive never changing behavior), matching the asset cache's
|
|
47
|
+
* exported-for-direct-testing precedent (`readBodyCapped`'s abort parameter). */
|
|
48
|
+
export function expandIpv6({ address: addr }) {
|
|
49
|
+
// `String#split` always returns a non-empty array, so index 0 is never
|
|
50
|
+
// `undefined` — the `!` narrows a `noUncheckedIndexedAccess` false
|
|
51
|
+
// positive, not a real fallback path.
|
|
52
|
+
const literal = addr.toLowerCase().split('%')[0];
|
|
53
|
+
if (!literal.includes(':'))
|
|
54
|
+
return null;
|
|
55
|
+
const s = foldIpv4Tail(literal);
|
|
56
|
+
if (s === null)
|
|
57
|
+
return null;
|
|
58
|
+
const halves = s.split('::');
|
|
59
|
+
if (halves.length > 2)
|
|
60
|
+
return null;
|
|
61
|
+
const parse = (p) => p === '' ? [] : p.split(':').map((g) => (/^[0-9a-f]{1,4}$/.test(g) ? parseInt(g, 16) : Number.NaN));
|
|
62
|
+
// `halves` always has at least one element (split guarantee).
|
|
63
|
+
const left = parse(halves[0]);
|
|
64
|
+
let groups;
|
|
65
|
+
if (halves.length === 1) {
|
|
66
|
+
groups = left;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// `halves.length === 2` here (the `> 2` case already returned above).
|
|
70
|
+
const right = parse(halves[1]);
|
|
71
|
+
const fill = 8 - left.length - right.length;
|
|
72
|
+
if (fill < 1)
|
|
73
|
+
return null;
|
|
74
|
+
groups = [...left, ...new Array(fill).fill(0), ...right];
|
|
75
|
+
}
|
|
76
|
+
if (!validIpv6Groups(groups))
|
|
77
|
+
return null;
|
|
78
|
+
return groups;
|
|
79
|
+
}
|
|
80
|
+
/** Fold a dotted tail while retaining the parser's defense-in-depth octet validation. */
|
|
81
|
+
function foldIpv4Tail(s) {
|
|
82
|
+
// Fold a trailing dotted IPv4 (`…:1.2.3.4`) into two hex groups.
|
|
83
|
+
const v4 = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(s);
|
|
84
|
+
if (v4) {
|
|
85
|
+
// `v4[2]` is the regex's second (mandatory, non-optional) capture group
|
|
86
|
+
// of a successful match — always defined.
|
|
87
|
+
const o = v4[2].split('.').map((n) => Number(n));
|
|
88
|
+
if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
89
|
+
return null;
|
|
90
|
+
// `o` is now a validated 4-element array — indices 0-3 always defined.
|
|
91
|
+
const hi = (((o[0] << 8) | o[1])).toString(16);
|
|
92
|
+
const lo = (((o[2] << 8) | o[3])).toString(16);
|
|
93
|
+
s = `${v4[1]}${hi}:${lo}`;
|
|
94
|
+
}
|
|
95
|
+
return s;
|
|
35
96
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
97
|
+
function validIpv6Groups(groups) {
|
|
98
|
+
return groups.length === 8 && groups.every(g => Number.isInteger(g) && g >= 0 && g <= 0xffff);
|
|
99
|
+
}
|
|
100
|
+
/** Classifies validated IPv4 bytes; shared by native and embedded-v4 literals. */
|
|
101
|
+
function classifyIpv4Bytes(bytes) {
|
|
102
|
+
const [a, b] = bytes;
|
|
103
|
+
if (a === 127)
|
|
104
|
+
return 'loopback';
|
|
105
|
+
// This range includes the cloud metadata address 169.254.169.254.
|
|
106
|
+
if (a === 169 && b === 254)
|
|
107
|
+
return 'link-local';
|
|
108
|
+
if (a === 0 || a >= 224)
|
|
109
|
+
return 'reserved'; // "this" network / unspecified; multicast + reserved
|
|
110
|
+
// The remaining blocked IPv4 ranges include RFC 6598 shared carrier space (100.64.0.0/10):
|
|
111
|
+
// it is not globally routable and belongs with RFC 1918 private ranges for egress decisions.
|
|
112
|
+
if (isPrivateIpv4Bytes(bytes))
|
|
113
|
+
return 'private';
|
|
114
|
+
return 'public';
|
|
115
|
+
}
|
|
116
|
+
function isPrivateIpv4Bytes([a, b]) {
|
|
117
|
+
if (a === 100)
|
|
118
|
+
return b >= 64 && b <= 127;
|
|
119
|
+
return a === 10 ||
|
|
46
120
|
(a === 172 && b >= 16 && b <= 31) ||
|
|
47
|
-
a
|
|
121
|
+
(a === 192 && b === 168);
|
|
48
122
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
123
|
+
// Each /96 prefix carries IPv4 in the final four bytes. Matching canonical
|
|
124
|
+
// bytes also handles expanded IPv6 and WHATWG URL's dotted-to-hex normalization.
|
|
125
|
+
const EMBEDDED_IPV4_PREFIXES = [
|
|
126
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // compatible ::/96 (except native ::/::1)
|
|
127
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255], // mapped ::ffff:0:0/96
|
|
128
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0], // translated ::ffff:0:0:0/96
|
|
129
|
+
[0, 100, 255, 155, 0, 0, 0, 0, 0, 0, 0, 0], // well-known NAT64 64:ff9b::/96
|
|
130
|
+
];
|
|
131
|
+
function hasEmbeddedIpv4(bytes) {
|
|
132
|
+
return EMBEDDED_IPV4_PREFIXES.some(prefix => hasPrefix(bytes, prefix));
|
|
133
|
+
}
|
|
134
|
+
function hasPrefix(bytes, prefix) {
|
|
135
|
+
return prefix.every((byte, index) => bytes[index] === byte);
|
|
136
|
+
}
|
|
137
|
+
// Teredo 2001::/32 obfuscates its client IPv4, and local-use NAT64 64:ff9b:1::/48 may embed
|
|
138
|
+
// IPv4 at any prefix length, so neither can be classified by position: both are refused.
|
|
139
|
+
const UNCLASSIFIABLE_TUNNEL_PREFIXES = [
|
|
140
|
+
[0x20, 0x01, 0, 0], // Teredo 2001::/32
|
|
141
|
+
[0, 0x64, 0xff, 0x9b, 0, 1], // local-use NAT64 64:ff9b:1::/48
|
|
142
|
+
];
|
|
143
|
+
/** Tunnel prefixes that reach IPv4 without a fixed /96 tail; null means not a tunnel prefix. */
|
|
144
|
+
function classifyTunnelIpv6(bytes) {
|
|
145
|
+
// 6to4 2002::/16 carries its IPv4 in bytes 2-5; a host with a 6to4 route encapsulates to it.
|
|
146
|
+
if (hasPrefix(bytes, [0x20, 0x02]))
|
|
147
|
+
return classifyIpv4Bytes(bytes.slice(2, 6));
|
|
148
|
+
if (UNCLASSIFIABLE_TUNNEL_PREFIXES.some(prefix => hasPrefix(bytes, prefix)))
|
|
149
|
+
return 'reserved';
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/** Classifies 16 IPv6 bytes, independent of compression or dotted/hex tail spelling. */
|
|
153
|
+
function classifyIpv6Bytes(bytes) {
|
|
154
|
+
// Native :: and ::1 retain their own meaning before the deprecated compatible-v4 range.
|
|
155
|
+
if (bytes.every(n => n === 0))
|
|
156
|
+
return 'reserved'; // :: (unspecified)
|
|
157
|
+
if (bytes.slice(0, 15).every(n => n === 0) && bytes[15] === 1)
|
|
158
|
+
return 'loopback';
|
|
159
|
+
// IPv4-mapped (::ffff:0:0/96) and deprecated IPv4-compatible (::/96)
|
|
160
|
+
// carry an embedded IPv4: classify its real range so ::ffff:7f00:1 is caught.
|
|
161
|
+
if (hasEmbeddedIpv4(bytes))
|
|
162
|
+
return classifyIpv4Bytes(bytes.slice(12));
|
|
163
|
+
const tunnel = classifyTunnelIpv6(bytes);
|
|
164
|
+
if (tunnel)
|
|
165
|
+
return tunnel;
|
|
166
|
+
if (bytes[0] === 255)
|
|
167
|
+
return 'reserved'; // multicast ff00::/8
|
|
168
|
+
const first = (bytes[0] << 8) | bytes[1];
|
|
169
|
+
if ((first & 0xffc0) === 0xfe80)
|
|
170
|
+
return 'link-local'; // fe80::/10 (fe80–febf)
|
|
171
|
+
if ((first & 0xfe00) === 0xfc00)
|
|
172
|
+
return 'private'; // unique-local fc00::/7
|
|
173
|
+
return 'public';
|
|
174
|
+
}
|
|
175
|
+
/** Pure literal classifier; null means malformed input or a hostname requiring DNS. */
|
|
176
|
+
export function classifyAddressLiteral({ address }, _options = {}) {
|
|
177
|
+
if (!address.includes(':')) {
|
|
178
|
+
const ipv4 = parseIpv4(address);
|
|
179
|
+
return ipv4 ? classifyIpv4Bytes(ipv4) : null;
|
|
61
180
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(value >>> 8) & 255,
|
|
71
|
-
value & 255,
|
|
72
|
-
].join('.');
|
|
181
|
+
const groups = expandIpv6({ address });
|
|
182
|
+
if (!groups)
|
|
183
|
+
return null;
|
|
184
|
+
// The parser's length/regex checks guarantee complete groups and bytes here;
|
|
185
|
+
// non-null assertions in byte classification narrow index types, not a runtime fallback.
|
|
186
|
+
// Parse once, then all range decisions operate on the same 16 canonical bytes.
|
|
187
|
+
const bytes = groups.flatMap(group => [group >> 8, group & 255]);
|
|
188
|
+
return classifyIpv6Bytes(bytes);
|
|
73
189
|
}
|
|
74
|
-
/** True for
|
|
190
|
+
/** True for localhost and literal loopback, including embedded IPv4 forms. */
|
|
75
191
|
export function isLoopbackApiHost({ hostname }) {
|
|
76
192
|
const host = normalizeBracketedIpv6(hostname);
|
|
77
|
-
|
|
78
|
-
return true;
|
|
79
|
-
if (isLoopbackIpv4(host))
|
|
80
|
-
return true;
|
|
81
|
-
const mapped = ipv4MappedToDotted(host);
|
|
82
|
-
return Boolean(mapped && isLoopbackIpv4(mapped));
|
|
193
|
+
return host === 'localhost' || classifyAddressLiteral({ address: host }) === 'loopback';
|
|
83
194
|
}
|
|
84
|
-
/**
|
|
195
|
+
/** Private/reserved literal hosts are blocked; configured local servers keep their loopback exception. */
|
|
85
196
|
export function isBlockedExternalApiHostname({ hostname }) {
|
|
86
197
|
const host = normalizeBracketedIpv6(hostname);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (isBlockedIpv4(host))
|
|
90
|
-
return true;
|
|
91
|
-
if (/^f[cd][0-9a-f]{2}:/i.test(host))
|
|
92
|
-
return true;
|
|
93
|
-
if (/^fe[89ab][0-9a-f]:/i.test(host))
|
|
94
|
-
return true;
|
|
95
|
-
const mapped = ipv4MappedToDotted(host);
|
|
96
|
-
return Boolean(mapped && isBlockedIpv4(mapped));
|
|
198
|
+
const addressClass = classifyAddressLiteral({ address: host });
|
|
199
|
+
return addressClass !== null && addressClass !== 'public' && addressClass !== 'loopback';
|
|
97
200
|
}
|
|
98
201
|
//# sourceMappingURL=endpoint-policy.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ObjectStore, ObjectStoreProvider } from '@jini-ai/core/object-store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a local-filesystem `ObjectStore` rooted at `rootDir`.
|
|
4
|
+
* @complexity Each call is O(1) syscalls beyond the read/write itself.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createLocalFsObjectStore(required: {
|
|
7
|
+
readonly rootDir: string;
|
|
8
|
+
}, _optional?: Record<string, never>): ObjectStore;
|
|
9
|
+
/**
|
|
10
|
+
* The `local` provider bound to one root directory (the host owns where bytes live; settings and
|
|
11
|
+
* credentials are ignored — local storage needs neither).
|
|
12
|
+
* @complexity O(1).
|
|
13
|
+
*/
|
|
14
|
+
export declare function createLocalFsObjectStoreProvider(required: {
|
|
15
|
+
readonly rootDir: string;
|
|
16
|
+
}, _optional?: Record<string, never>): ObjectStoreProvider;
|
|
17
|
+
//# sourceMappingURL=local-fs.d.ts.map
|