@jini-ai/platform 0.5.9 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/asset-cache.js +26 -25
  3. package/dist/blob-storage.d.ts +7 -2
  4. package/dist/blob-storage.js +99 -59
  5. package/dist/child-environment.d.ts +36 -0
  6. package/dist/child-environment.js +68 -0
  7. package/dist/download.js +192 -148
  8. package/dist/fs/atomic-write.js +105 -93
  9. package/dist/fs/boot-identity.d.ts +16 -0
  10. package/dist/fs/boot-identity.js +38 -17
  11. package/dist/fs/directory-home.js +6 -0
  12. package/dist/fs/directory-registry.d.ts +1 -1
  13. package/dist/fs/directory-registry.js +23 -16
  14. package/dist/fs/directory-running-lock.d.ts +1 -1
  15. package/dist/fs/directory-running-lock.js +18 -7
  16. package/dist/fs/directory-sync.d.ts +1 -1
  17. package/dist/fs/directory-sync.js +64 -51
  18. package/dist/fs/directory-user-config.js +108 -67
  19. package/dist/fs/directory-workspace.d.ts +11 -0
  20. package/dist/fs/directory-workspace.js +16 -11
  21. package/dist/fs/durable-json.js +49 -37
  22. package/dist/fs/file-lock.js +214 -149
  23. package/dist/fs/guarded-reader.js +47 -35
  24. package/dist/fs/user-data-directories.d.ts +1 -1
  25. package/dist/fs/user-data-directories.js +25 -9
  26. package/dist/fs/verified-directory-move.d.ts +13 -11
  27. package/dist/fs/verified-directory-move.js +105 -68
  28. package/dist/fs/verified-file-relocation.d.ts +1 -1
  29. package/dist/fs/verified-file-relocation.js +111 -94
  30. package/dist/http/guarded/client.d.ts +3 -4
  31. package/dist/http/guarded/client.js +50 -60
  32. package/dist/http/guarded/fetch-adapter.d.ts +2 -3
  33. package/dist/http/guarded/fetch-adapter.js +26 -15
  34. package/dist/mail/builtin-providers.d.ts +39 -0
  35. package/dist/mail/builtin-providers.js +60 -0
  36. package/dist/mail/feedback.d.ts +84 -0
  37. package/dist/mail/feedback.js +90 -0
  38. package/dist/mail/index.d.ts +11 -0
  39. package/dist/mail/index.js +6 -0
  40. package/dist/mail/policy.d.ts +77 -0
  41. package/dist/mail/policy.js +155 -0
  42. package/dist/mail/ports.d.ts +56 -7
  43. package/dist/mail/provider.d.ts +117 -0
  44. package/dist/mail/provider.js +74 -0
  45. package/dist/mail/rate-limit.d.ts +33 -0
  46. package/dist/mail/rate-limit.js +35 -0
  47. package/dist/mail/sender-identity.d.ts +68 -0
  48. package/dist/mail/sender-identity.js +50 -0
  49. package/dist/mail/types.d.ts +9 -0
  50. package/dist/net/address.d.ts +9 -23
  51. package/dist/net/address.js +28 -133
  52. package/dist/net/endpoint-policy.d.ts +26 -2
  53. package/dist/net/endpoint-policy.js +159 -56
  54. package/dist/object-store/index.d.ts +2 -0
  55. package/dist/object-store/index.js +2 -0
  56. package/dist/object-store/local-fs.d.ts +17 -0
  57. package/dist/object-store/local-fs.js +146 -0
  58. package/dist/process.js +2 -0
  59. package/dist/proxy-env.js +131 -135
  60. package/dist/secrets/credential-sets/contracts.d.ts +1 -0
  61. package/dist/secrets/credential-sets/contracts.js +1 -0
  62. package/dist/secrets/credential-sets/index.d.ts +1 -0
  63. package/dist/secrets/credential-sets/index.js +1 -0
  64. package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
  65. package/dist/secrets/credential-sets/provider-matches.js +61 -0
  66. package/dist/secrets/credential-sets/types.d.ts +7 -0
  67. package/dist/secrets/credential-sets/vault.meta.js +58 -20
  68. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
  69. package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
  70. package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
  71. package/dist/secrets/credential-sets/vault.service.js +131 -61
  72. package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
  73. package/dist/secrets/credential-sets/vault.types.js +22 -8
  74. package/dist/secrets/keyring.env.js +14 -10
  75. package/dist/toolchain.js +119 -91
  76. package/dist/web/html-document.js +73 -69
  77. package/dist/web/html-to-markdown.js +127 -102
  78. package/dist/web/web-page.js +160 -80
  79. package/package.json +11 -3
@@ -1,26 +1,12 @@
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 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';
@@ -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
- /** Expand an IPv6 literal (with `::` compression and/or an embedded dotted
4
- * IPv4 tail) into its eight 16-bit groups, or null if it does not parse.
5
- * Node's URL parser normalizes mapped literals to hex (`::ffff:127.0.0.1` →
6
- * `::ffff:7f00:1`), so a string regex can't reliably spot the mapped range —
7
- * we have to canonicalize.
8
- *
9
- * Exported (previously module-private) so its parse-failure guards are
10
- * directly unit-testable: the IP classifier and guarded client gate
11
- * calls behind `net.isIP(addr) === 6`, and Node's `isIP` already fully
12
- * validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
13
- * returning 6 — empirically re-verified this session by fuzzing ~10k
14
- * `isIP`-accepted v6 literals through this function's guards with zero
15
- * hits — so none of these guards are reachable through that path. They stay
16
- * as real defense-in-depth (fail closed with `null` rather than trust a
17
- * platform primitive never changing behavior), matching the asset cache's
18
- * exported-for-direct-testing precedent (`readBodyCapped`'s abort parameter). */
19
- export function expandIpv6({ address: addr }) {
20
- // `String#split` always returns a non-empty array, so index 0 is never
21
- // `undefined` — the `!` narrows a `noUncheckedIndexedAccess` false
22
- // positive, not a real fallback path.
23
- let s = addr.toLowerCase().split('%')[0];
24
- if (!s.includes(':'))
25
- return null;
26
- // Fold a trailing dotted IPv4 (`…:1.2.3.4`) into two hex groups.
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 any address an external CDN must never resolve to: loopback,
62
- * link-local, private, CGNAT, ULA, multicast, or unspecified. An unparseable
63
- * input is treated as unsafe. */
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
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
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
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
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
- // isBlockedIpv4 parses them. Strips one or more trailing dots.
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
- function isLoopbackIpv4(hostname) {
33
- const parts = parseIpv4(hostname);
34
- return Boolean(parts && parts[0] === 127);
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 isBlockedIpv4(hostname) {
37
- const parts = parseIpv4(hostname);
38
- if (!parts)
39
- return false;
40
- const [a, b] = parts;
41
- return (a === 0 ||
42
- (a === 100 && b >= 64 && b <= 127) ||
43
- (a === 169 && b === 254) ||
44
- a === 10 ||
45
- (a === 192 && b === 168) ||
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 >= 224);
121
+ (a === 192 && b === 168);
48
122
  }
49
- /** `::ffff:10.0.0.1` and `::ffff:a00:1` denote the same address; return dotted form so the v4 rules apply. */
50
- function ipv4MappedToDotted(hostname) {
51
- const host = normalizeBracketedIpv6(hostname);
52
- const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
53
- if (!mapped)
54
- return null;
55
- if (parseIpv4(mapped.toLowerCase()))
56
- return mapped.toLowerCase();
57
- const hexParts = mapped.split(':');
58
- if (hexParts.length !== 2 ||
59
- !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
60
- return null;
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
- // Non-null assertions, not a runtime guard: the length/regex checks above
63
- // already guarantee exactly two non-empty hex segments here.
64
- const hi = hexParts[0];
65
- const lo = hexParts[1];
66
- const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
67
- return [
68
- (value >>> 24) & 255,
69
- (value >>> 16) & 255,
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 `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
190
+ /** True for localhost and literal loopback, including embedded IPv4 forms. */
75
191
  export function isLoopbackApiHost({ hostname }) {
76
192
  const host = normalizeBracketedIpv6(hostname);
77
- if (host === 'localhost' || host === '::1')
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
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
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
- if (host === '::')
88
- return true;
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,2 @@
1
+ export { createLocalFsObjectStore, createLocalFsObjectStoreProvider } from './local-fs.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { createLocalFsObjectStore, createLocalFsObjectStoreProvider } from './local-fs.js';
2
+ //# sourceMappingURL=index.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