@fgv/ts-extras 5.1.0-46 → 5.1.0-48
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/LICENSE +1 -1
- package/README.md +90 -0
- package/dist/index.browser.js +3 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/packlets/safer-fetch/addressClassification.js +438 -0
- package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
- package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/dist/packlets/safer-fetch/contentType.js +120 -0
- package/dist/packlets/safer-fetch/contentType.js.map +1 -0
- package/dist/packlets/safer-fetch/deadline.js +254 -0
- package/dist/packlets/safer-fetch/deadline.js.map +1 -0
- package/dist/packlets/safer-fetch/defaults.js +131 -0
- package/dist/packlets/safer-fetch/defaults.js.map +1 -0
- package/dist/packlets/safer-fetch/failureReason.js +21 -0
- package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
- package/dist/packlets/safer-fetch/guards.js +148 -0
- package/dist/packlets/safer-fetch/guards.js.map +1 -0
- package/dist/packlets/safer-fetch/index.browser.js +58 -0
- package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
- package/dist/packlets/safer-fetch/index.js +52 -0
- package/dist/packlets/safer-fetch/index.js.map +1 -0
- package/dist/packlets/safer-fetch/model.js +21 -0
- package/dist/packlets/safer-fetch/model.js.map +1 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/dist/packlets/safer-fetch/redirect.js +93 -0
- package/dist/packlets/safer-fetch/redirect.js.map +1 -0
- package/dist/packlets/safer-fetch/retry.js +188 -0
- package/dist/packlets/safer-fetch/retry.js.map +1 -0
- package/dist/packlets/safer-fetch/saferFetch.js +755 -0
- package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/dist/packlets/safer-fetch/transport.js +50 -0
- package/dist/packlets/safer-fetch/transport.js.map +1 -0
- package/dist/ts-extras.d.ts +1337 -0
- package/lib/index.browser.d.ts +2 -1
- package/lib/index.browser.d.ts.map +1 -1
- package/lib/index.browser.js +4 -1
- package/lib/index.browser.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
- package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressClassification.js +441 -0
- package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
- package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
- package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/contentType.js +127 -0
- package/lib/packlets/safer-fetch/contentType.js.map +1 -0
- package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
- package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/deadline.js +258 -0
- package/lib/packlets/safer-fetch/deadline.js.map +1 -0
- package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
- package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/defaults.js +134 -0
- package/lib/packlets/safer-fetch/defaults.js.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.js +22 -0
- package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
- package/lib/packlets/safer-fetch/guards.d.ts +62 -0
- package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/guards.js +153 -0
- package/lib/packlets/safer-fetch/guards.js.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.js +82 -0
- package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
- package/lib/packlets/safer-fetch/index.d.ts +27 -0
- package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.js +78 -0
- package/lib/packlets/safer-fetch/index.js.map +1 -0
- package/lib/packlets/safer-fetch/model.d.ts +373 -0
- package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/model.js +22 -0
- package/lib/packlets/safer-fetch/model.js.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
- package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/redirect.js +98 -0
- package/lib/packlets/safer-fetch/redirect.js.map +1 -0
- package/lib/packlets/safer-fetch/retry.d.ts +162 -0
- package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/retry.js +197 -0
- package/lib/packlets/safer-fetch/retry.js.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.js +760 -0
- package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/lib/packlets/safer-fetch/transport.d.ts +18 -0
- package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/transport.js +53 -0
- package/lib/packlets/safer-fetch/transport.js.map +1 -0
- package/package.json +17 -7
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -86,6 +86,96 @@ const rawContents = file.value.getRawContents(); // Raw string
|
|
|
86
86
|
|
|
87
87
|
**Note**: This implementation uses Node.js-specific dependencies (AdmZip, Buffer). For browser environments, see the browser-specific implementations in individual projects.
|
|
88
88
|
|
|
89
|
+
### Safer Fetch
|
|
90
|
+
|
|
91
|
+
An HTTP fetch primitive with an explicit threat model, exported from `@fgv/ts-extras/safer-fetch`.
|
|
92
|
+
It is deliberately **not** a thin boundary over an upstream library — `fetch` is a platform global
|
|
93
|
+
and there is nothing to wrap. The opinion is the product: the deadlines, the scheme refusal, the
|
|
94
|
+
streaming size cap, the redirect posture, and the required address guard are the deliverable.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {
|
|
98
|
+
saferFetchJson,
|
|
99
|
+
blockPrivateNetworks,
|
|
100
|
+
allowContentTypes
|
|
101
|
+
} from '@fgv/ts-extras/safer-fetch';
|
|
102
|
+
|
|
103
|
+
const result = await saferFetchJson('https://api.example.com/thing', {
|
|
104
|
+
// Required, with no default: omitting it is a compile error, so no call site can inherit a
|
|
105
|
+
// guarantee it was never given. Every call site's posture is greppable in one search.
|
|
106
|
+
addressGuard: blockPrivateNetworks(),
|
|
107
|
+
responseHeadersGuard: allowContentTypes(['application/json']).orThrow(),
|
|
108
|
+
timeoutMs: 10_000,
|
|
109
|
+
maxResponseBytes: 1024 * 1024,
|
|
110
|
+
retry: { attempts: 2 } // off unless asked for
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`addressGuard` has no default. `allowAnyAddress()` is the named, deliberately uncomfortable
|
|
115
|
+
opt-out, and it is the only honest choice in a browser.
|
|
116
|
+
|
|
117
|
+
#### The guarantee table
|
|
118
|
+
|
|
119
|
+
This table is the artifact that keeps the primitive honest. It is reproduced verbatim in
|
|
120
|
+
`@fgv/ts-web-extras`'s README.
|
|
121
|
+
|
|
122
|
+
| Property | Node (`@fgv/ts-extras/safer-fetch`) | Browser (`@fgv/ts-web-extras`) |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| Overall + headers deadline | ✅ | ✅ |
|
|
125
|
+
| Streaming size cap, enforced during read | ✅ | ✅ |
|
|
126
|
+
| Content-type gate before body read | ✅ | ✅ |
|
|
127
|
+
| Structured failure taxonomy | ✅ | ✅ |
|
|
128
|
+
| Retry with idempotency + budget rules | ✅ | ✅ |
|
|
129
|
+
| Scheme allowlist on URL₀ | ✅ | ✅ |
|
|
130
|
+
| Host / port allowlist on URL₀ | ✅ (`blockPrivateNetworks({ allowHosts, allowPorts })`) | ✅ (caller-supplied guard) |
|
|
131
|
+
| **Resolved-address (private-IP) guard** | ✅ | ❌ **impossible** — no DNS API |
|
|
132
|
+
| **Per-hop revalidation of redirects** | ✅ | ❌ **impossible** — opaque redirect |
|
|
133
|
+
| **Credential stripping on cross-origin hop** | ✅ | n/a — platform does it |
|
|
134
|
+
| Reject-all-redirects mode | ✅ | ✅ (enforced; surfaces as `'redirect-opaque'`) |
|
|
135
|
+
| DNS-rebinding resistance | ❌ **documented limit** | ❌ |
|
|
136
|
+
|
|
137
|
+
Two rows differ from the design document they came from, and the difference is deliberate:
|
|
138
|
+
|
|
139
|
+
- **Host / port allowlist.** `blockPrivateNetworks` takes `allowHosts` / `allowPorts` /
|
|
140
|
+
`allowInsecureHttp`. A host allowlist is the *recommended* posture, because it shrinks the
|
|
141
|
+
DNS-rebinding exposure below to "an allowlisted host's own resolver is hostile". `https:` is
|
|
142
|
+
required unless `allowInsecureHttp` is set; there is no default port restriction, since a
|
|
143
|
+
default of `{443}` would reject the very common `:8443` public endpoint with a failure that
|
|
144
|
+
reads as an SSRF block.
|
|
145
|
+
- **Reject-all-redirects.** Both runtimes use `redirect: 'manual'` and reject the redirect
|
|
146
|
+
themselves. The *guarantee* is identical; the failure **reason** is not — Node reports
|
|
147
|
+
`'redirect-rejected'` with the status, a browser reports `'redirect-opaque'` because an opaque
|
|
148
|
+
response has no status to report. Callers branching on the reason under `'reject'` must handle
|
|
149
|
+
both.
|
|
150
|
+
|
|
151
|
+
#### Stated limits
|
|
152
|
+
|
|
153
|
+
Each of these is a hole in a guarantee this package makes, and each is documented rather than
|
|
154
|
+
implied.
|
|
155
|
+
|
|
156
|
+
- **DNS rebinding** — the guard validates a resolved address and the transport then re-resolves,
|
|
157
|
+
so hostile DNS can answer the two lookups differently. Closing it needs a pinning transport;
|
|
158
|
+
the seam (`IGuardVerdict.pinnedAddress` + `IFetchTransport`) exists, and
|
|
159
|
+
`platformFetchTransport` **fails** rather than silently ignoring a pin it cannot honor. A
|
|
160
|
+
strict `allowHosts` list is the recommended mitigation.
|
|
161
|
+
- **The browser has no SSRF guard at all** — not a partial guarantee, an absent one. See the
|
|
162
|
+
table.
|
|
163
|
+
- **Time-of-check/time-of-use beyond DNS** — the guard authorizes a *destination*, never the
|
|
164
|
+
*content* that comes back.
|
|
165
|
+
- **The failure taxonomy is an information-disclosure surface** — a `'blocked-by-guard'` detail
|
|
166
|
+
names the URL, the hop and the guard, which is an internal-network scanning oracle if echoed
|
|
167
|
+
to an untrusted caller. Log it; return a coarse code.
|
|
168
|
+
- **No egress accounting** — no per-host rate limiting or quota. A retry policy is not a rate
|
|
169
|
+
limiter.
|
|
170
|
+
- **Loopback is blocked by default**, including for this repo's own local-development path. A
|
|
171
|
+
caller who wants it says so, and every deviation is independently greppable:
|
|
172
|
+
`blockPrivateNetworks({ allowLoopback: true, allowInsecureHttp: true, allowHosts: ['localhost'], allowPorts: [11434] })`.
|
|
173
|
+
- **HTTP semantics not implemented** — no cookie jar, no cache, no conditional requests, no
|
|
174
|
+
proxy configuration beyond what the platform picks up from the environment.
|
|
175
|
+
|
|
176
|
+
If your deployment has an egress proxy or firewall, that control is strictly stronger than this
|
|
177
|
+
one; this is defense in depth for deployments that do not.
|
|
178
|
+
|
|
89
179
|
### Converters
|
|
90
180
|
|
|
91
181
|
Type-safe data conversion utilities for transforming between different data formats while maintaining type safety.
|
package/dist/index.browser.js
CHANGED
|
@@ -31,11 +31,13 @@ import * as Hash from './packlets/hash/index.browser';
|
|
|
31
31
|
import * as Mustache from './packlets/mustache';
|
|
32
32
|
// eslint-disable-next-line @rushstack/packlets/mechanics
|
|
33
33
|
import * as RecordJar from './packlets/record-jar/index.browser';
|
|
34
|
+
// eslint-disable-next-line @rushstack/packlets/mechanics
|
|
35
|
+
import * as SaferFetch from './packlets/safer-fetch/index.browser';
|
|
34
36
|
import * as Yaml from './packlets/yaml';
|
|
35
37
|
import * as ZipFileTree from './packlets/zip-file-tree';
|
|
36
38
|
import { Converters } from './packlets/conversion';
|
|
37
39
|
// Browser-safe exports - Node.js crypto-based providers excluded
|
|
38
40
|
// Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto
|
|
39
|
-
export { AiAssist, Converters, CryptoUtils, CryptoUtils as Crypto, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };
|
|
41
|
+
export { AiAssist, Converters, CryptoUtils, CryptoUtils as Crypto, Csv, Experimental, Hash, Mustache, RecordJar, SaferFetch, Yaml, ZipFileTree };
|
|
40
42
|
/* c8 ignore stop */
|
|
41
43
|
//# sourceMappingURL=index.browser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,kFAAkF;AAClF,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,yDAAyD;AACzD,OAAO,KAAK,WAAW,MAAM,uCAAuC,CAAC;AACrE,yDAAyD;AACzD,OAAO,KAAK,GAAG,MAAM,8BAA8B,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,yDAAyD;AACzD,OAAO,KAAK,IAAI,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,yDAAyD;AACzD,OAAO,KAAK,SAAS,MAAM,qCAAqC,CAAC;AACjE,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,iEAAiE;AACjE,uEAAuE;AACvE,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,IAAI,MAAM,EACrB,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,WAAW,EACZ,CAAC;AACF,oBAAoB","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* c8 ignore start - Browser-specific export used conditionally in package.json */\nimport * as AiAssist from './packlets/ai-assist';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as CryptoUtils from './packlets/crypto-utils/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Csv from './packlets/csv/index.browser';\nimport * as Experimental from './packlets/experimental';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Hash from './packlets/hash/index.browser';\nimport * as Mustache from './packlets/mustache';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as RecordJar from './packlets/record-jar/index.browser';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\n// Browser-safe exports - Node.js crypto-based providers excluded\n// Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n CryptoUtils as Crypto,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n Yaml,\n ZipFileTree\n};\n/* c8 ignore stop */\n"]}
|
|
1
|
+
{"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,kFAAkF;AAClF,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,yDAAyD;AACzD,OAAO,KAAK,WAAW,MAAM,uCAAuC,CAAC;AACrE,yDAAyD;AACzD,OAAO,KAAK,GAAG,MAAM,8BAA8B,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,yDAAyD;AACzD,OAAO,KAAK,IAAI,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,yDAAyD;AACzD,OAAO,KAAK,SAAS,MAAM,qCAAqC,CAAC;AACjE,yDAAyD;AACzD,OAAO,KAAK,UAAU,MAAM,sCAAsC,CAAC;AACnE,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,iEAAiE;AACjE,uEAAuE;AACvE,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,IAAI,MAAM,EACrB,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,CAAC;AACF,oBAAoB","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* c8 ignore start - Browser-specific export used conditionally in package.json */\nimport * as AiAssist from './packlets/ai-assist';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as CryptoUtils from './packlets/crypto-utils/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Csv from './packlets/csv/index.browser';\nimport * as Experimental from './packlets/experimental';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Hash from './packlets/hash/index.browser';\nimport * as Mustache from './packlets/mustache';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as RecordJar from './packlets/record-jar/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as SaferFetch from './packlets/safer-fetch/index.browser';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\n// Browser-safe exports - Node.js crypto-based providers excluded\n// Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n CryptoUtils as Crypto,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n SaferFetch,\n Yaml,\n ZipFileTree\n};\n/* c8 ignore stop */\n"]}
|
package/dist/index.js
CHANGED
|
@@ -26,8 +26,9 @@ import * as Experimental from './packlets/experimental';
|
|
|
26
26
|
import * as Hash from './packlets/hash';
|
|
27
27
|
import * as Mustache from './packlets/mustache';
|
|
28
28
|
import * as RecordJar from './packlets/record-jar';
|
|
29
|
+
import * as SaferFetch from './packlets/safer-fetch';
|
|
29
30
|
import * as Yaml from './packlets/yaml';
|
|
30
31
|
import * as ZipFileTree from './packlets/zip-file-tree';
|
|
31
32
|
import { Converters } from './packlets/conversion';
|
|
32
|
-
export { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };
|
|
33
|
+
export { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, SaferFetch, Yaml, ZipFileTree };
|
|
33
34
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,CAAC","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as AiAssist from './packlets/ai-assist';\nimport * as CryptoUtils from './packlets/crypto-utils';\nimport * as Csv from './packlets/csv';\nimport * as Experimental from './packlets/experimental';\nimport * as Hash from './packlets/hash';\nimport * as Mustache from './packlets/mustache';\nimport * as RecordJar from './packlets/record-jar';\nimport * as SaferFetch from './packlets/safer-fetch';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n SaferFetch,\n Yaml,\n ZipFileTree\n};\n"]}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Copyright (c) 2026 Erik Fortune
|
|
2
|
+
//
|
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
// furnished to do so, subject to the following conditions:
|
|
9
|
+
//
|
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
// copies or substantial portions of the Software.
|
|
12
|
+
//
|
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
// SOFTWARE.
|
|
20
|
+
// Address classification is prefix arithmetic: every range test is a mask and a
|
|
21
|
+
// compare, and every IPv6 group/byte conversion is a shift. Spelling those as
|
|
22
|
+
// arithmetic instead would be strictly harder to audit against the RFCs, which
|
|
23
|
+
// is the opposite of what this file needs. The repo already takes per-site
|
|
24
|
+
// disables for the same reason in `crypto-utils/keystore` and `ts-random`.
|
|
25
|
+
/* eslint-disable no-bitwise */
|
|
26
|
+
import { fail, succeed } from '@fgv/ts-utils';
|
|
27
|
+
/**
|
|
28
|
+
* IPv4 special-purpose ranges, most specific first. The broadcast address is
|
|
29
|
+
* listed ahead of `240.0.0.0/4` because it falls inside it.
|
|
30
|
+
*/
|
|
31
|
+
const IPV4_RANGES = [
|
|
32
|
+
{ network: 0xffffffff, prefixLength: 32, classification: 'broadcast' },
|
|
33
|
+
{ network: 0x00000000, prefixLength: 8, classification: 'unspecified' },
|
|
34
|
+
{ network: 0x0a000000, prefixLength: 8, classification: 'private' },
|
|
35
|
+
{ network: 0x64400000, prefixLength: 10, classification: 'carrier-grade-nat' },
|
|
36
|
+
{ network: 0x7f000000, prefixLength: 8, classification: 'loopback' },
|
|
37
|
+
{ network: 0xa9fe0000, prefixLength: 16, classification: 'link-local' },
|
|
38
|
+
{ network: 0xac100000, prefixLength: 12, classification: 'private' },
|
|
39
|
+
{ network: 0xc0000000, prefixLength: 24, classification: 'protocol-assignment' },
|
|
40
|
+
{ network: 0xc0000200, prefixLength: 24, classification: 'documentation' },
|
|
41
|
+
{ network: 0xc0586300, prefixLength: 24, classification: 'reserved' },
|
|
42
|
+
{ network: 0xc0a80000, prefixLength: 16, classification: 'private' },
|
|
43
|
+
{ network: 0xc6120000, prefixLength: 15, classification: 'benchmarking' },
|
|
44
|
+
{ network: 0xc6336400, prefixLength: 24, classification: 'documentation' },
|
|
45
|
+
{ network: 0xcb007100, prefixLength: 24, classification: 'documentation' },
|
|
46
|
+
{ network: 0xe0000000, prefixLength: 4, classification: 'multicast' },
|
|
47
|
+
{ network: 0xf0000000, prefixLength: 4, classification: 'reserved' }
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* IPv6 special-purpose ranges, most specific first. Ranges whose addresses
|
|
51
|
+
* embed an IPv4 address are handled separately, before this table is consulted,
|
|
52
|
+
* and anything outside global unicast (`2000::/3`) that matches nothing here is
|
|
53
|
+
* classified `'reserved'` — so this table only needs to name the ranges that
|
|
54
|
+
* deserve a more specific classification than that fallback gives them.
|
|
55
|
+
*/
|
|
56
|
+
const IPV6_RANGES = [
|
|
57
|
+
{ prefix: [0x20, 0x01, 0x0d, 0xb8], prefixLength: 32, classification: 'documentation' },
|
|
58
|
+
{ prefix: [0x20, 0x01, 0x00], prefixLength: 23, classification: 'protocol-assignment' },
|
|
59
|
+
{ prefix: [0x3f, 0xff, 0x00], prefixLength: 20, classification: 'documentation' },
|
|
60
|
+
{ prefix: [0xfc], prefixLength: 7, classification: 'unique-local' },
|
|
61
|
+
{ prefix: [0xfe, 0x80], prefixLength: 10, classification: 'link-local' },
|
|
62
|
+
{ prefix: [0xff], prefixLength: 8, classification: 'multicast' }
|
|
63
|
+
];
|
|
64
|
+
/** The first three bits of a global unicast IPv6 address (`2000::/3`). */
|
|
65
|
+
const IPV6_GLOBAL_UNICAST_MASK = 0xe0;
|
|
66
|
+
const IPV6_GLOBAL_UNICAST_VALUE = 0x20;
|
|
67
|
+
/**
|
|
68
|
+
* Parses one component of an IPv4 literal using `inet_aton` conventions: a
|
|
69
|
+
* `0x`/`0X` prefix selects hexadecimal, an otherwise-leading `0` selects octal,
|
|
70
|
+
* and anything else is decimal.
|
|
71
|
+
*
|
|
72
|
+
* This is deliberately permissive about *radix* because the WHATWG URL parser
|
|
73
|
+
* is: `http://0177.0.0.1/` and `http://2130706433/` are both `127.0.0.1`, and a
|
|
74
|
+
* classifier that only understands dotted decimal is bypassed by either.
|
|
75
|
+
*/
|
|
76
|
+
function parseIpv4Component(text) {
|
|
77
|
+
if (text.length === 0) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
let radix = 10;
|
|
81
|
+
let digits = text;
|
|
82
|
+
if (text.length > 1 && (text[1] === 'x' || text[1] === 'X') && text[0] === '0') {
|
|
83
|
+
radix = 16;
|
|
84
|
+
digits = text.slice(2);
|
|
85
|
+
}
|
|
86
|
+
else if (text.length > 1 && text[0] === '0') {
|
|
87
|
+
radix = 8;
|
|
88
|
+
digits = text.slice(1);
|
|
89
|
+
}
|
|
90
|
+
// The WHATWG IPv4 number parser returns 0 when stripping the radix prefix
|
|
91
|
+
// leaves nothing behind, so a bare `0x` is zero: `127.0x.1` is `127.0.0.1`.
|
|
92
|
+
// Rejecting it here would leave a live encoding bypass.
|
|
93
|
+
if (digits.length === 0) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const allowed = radix === 16 ? /^[0-9a-fA-F]+$/ : radix === 8 ? /^[0-7]+$/ : /^[0-9]+$/;
|
|
97
|
+
if (!allowed.test(digits)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const value = Number.parseInt(digits, radix);
|
|
101
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parses an IPv4 literal in any of the forms the platform URL parser accepts
|
|
105
|
+
* (`a.b.c.d`, `a.b.c`, `a.b`, `a`), returning the address as an unsigned 32-bit
|
|
106
|
+
* value. The final component absorbs all remaining bytes, so `127.1` is
|
|
107
|
+
* `127.0.0.1` and `2130706433` is also `127.0.0.1`.
|
|
108
|
+
*/
|
|
109
|
+
function parseIpv4(text) {
|
|
110
|
+
const parts = text.split('.');
|
|
111
|
+
if (parts.length > 4) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const values = [];
|
|
115
|
+
for (const part of parts) {
|
|
116
|
+
const value = parseIpv4Component(part);
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
values.push(value);
|
|
121
|
+
}
|
|
122
|
+
const leading = values.length - 1;
|
|
123
|
+
for (let i = 0; i < leading; i++) {
|
|
124
|
+
if (values[i] > 0xff) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (values[leading] > Math.pow(256, 4 - leading) - 1) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
let address = values[leading];
|
|
132
|
+
for (let i = 0; i < leading; i++) {
|
|
133
|
+
address += values[i] * Math.pow(256, 3 - i);
|
|
134
|
+
}
|
|
135
|
+
return address >>> 0;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parses a strict dotted-quad IPv4 literal — exactly four decimal components,
|
|
139
|
+
* each `0..255`, with no octal or hexadecimal forms. This is the grammar RFC
|
|
140
|
+
* 4291 permits for the IPv4 tail of an IPv6 literal, and it is deliberately
|
|
141
|
+
* stricter than {@link parseIpv4}: an unparseable literal fails closed.
|
|
142
|
+
*/
|
|
143
|
+
function parseStrictDottedQuad(text) {
|
|
144
|
+
const parts = text.split('.');
|
|
145
|
+
if (parts.length !== 4) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
let address = 0;
|
|
149
|
+
for (const part of parts) {
|
|
150
|
+
if (!/^(0|[1-9][0-9]{0,2})$/.test(part)) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const value = Number.parseInt(part, 10);
|
|
154
|
+
if (value > 0xff) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
address = address * 256 + value;
|
|
158
|
+
}
|
|
159
|
+
return address >>> 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parses a colon-separated run of IPv6 groups, allowing a trailing embedded
|
|
163
|
+
* dotted-quad IPv4 literal which expands to the final two groups.
|
|
164
|
+
*
|
|
165
|
+
* @param allowEmbeddedIpv4 - whether this run may end in a dotted-quad IPv4
|
|
166
|
+
* literal. Only the run that ends the whole address may, so the run *before* a
|
|
167
|
+
* `::` never does: `1.2.3.4::` is malformed, not `102:304::`.
|
|
168
|
+
*/
|
|
169
|
+
function parseIpv6Groups(text, allowEmbeddedIpv4) {
|
|
170
|
+
if (text.length === 0) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const parts = text.split(':');
|
|
174
|
+
const groups = [];
|
|
175
|
+
for (let i = 0; i < parts.length; i++) {
|
|
176
|
+
const part = parts[i];
|
|
177
|
+
if (part.indexOf('.') >= 0) {
|
|
178
|
+
if (!allowEmbeddedIpv4 || i !== parts.length - 1) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const embedded = parseStrictDottedQuad(part);
|
|
182
|
+
if (embedded === undefined) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
groups.push((embedded >>> 16) & 0xffff, embedded & 0xffff);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
groups.push(Number.parseInt(part, 16));
|
|
192
|
+
}
|
|
193
|
+
return groups;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Parses an IPv6 literal into its 16 constituent bytes.
|
|
197
|
+
*/
|
|
198
|
+
function parseIpv6(text) {
|
|
199
|
+
const doubleColon = text.indexOf('::');
|
|
200
|
+
let groups;
|
|
201
|
+
if (doubleColon >= 0) {
|
|
202
|
+
if (text.indexOf('::', doubleColon + 1) >= 0) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const head = parseIpv6Groups(text.slice(0, doubleColon), false);
|
|
206
|
+
const tail = parseIpv6Groups(text.slice(doubleColon + 2), true);
|
|
207
|
+
if (head === undefined || tail === undefined) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const elided = 8 - head.length - tail.length;
|
|
211
|
+
if (elided < 1) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
groups = head.concat(new Array(elided).fill(0), tail);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
groups = parseIpv6Groups(text, true);
|
|
218
|
+
if (groups === undefined || groups.length !== 8) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const bytes = new Uint8Array(16);
|
|
223
|
+
for (let i = 0; i < 8; i++) {
|
|
224
|
+
bytes[i * 2] = (groups[i] >>> 8) & 0xff;
|
|
225
|
+
bytes[i * 2 + 1] = groups[i] & 0xff;
|
|
226
|
+
}
|
|
227
|
+
return bytes;
|
|
228
|
+
}
|
|
229
|
+
function ipv4ToString(address) {
|
|
230
|
+
return [(address >>> 24) & 0xff, (address >>> 16) & 0xff, (address >>> 8) & 0xff, address & 0xff].join('.');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Renders the RFC 5952 canonical text form of an IPv6 address: lowercase, no
|
|
234
|
+
* leading zeros in a group, and the leftmost longest run of two or more
|
|
235
|
+
* all-zero groups replaced by `::`.
|
|
236
|
+
*/
|
|
237
|
+
function ipv6ToString(bytes) {
|
|
238
|
+
const groups = [];
|
|
239
|
+
for (let i = 0; i < 8; i++) {
|
|
240
|
+
groups.push((bytes[i * 2] << 8) | bytes[i * 2 + 1]);
|
|
241
|
+
}
|
|
242
|
+
let bestStart = -1;
|
|
243
|
+
let bestLength = 0;
|
|
244
|
+
let runStart = -1;
|
|
245
|
+
let runLength = 0;
|
|
246
|
+
for (let i = 0; i < 8; i++) {
|
|
247
|
+
if (groups[i] === 0) {
|
|
248
|
+
if (runStart < 0) {
|
|
249
|
+
runStart = i;
|
|
250
|
+
runLength = 0;
|
|
251
|
+
}
|
|
252
|
+
runLength++;
|
|
253
|
+
if (runLength > bestLength) {
|
|
254
|
+
bestStart = runStart;
|
|
255
|
+
bestLength = runLength;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
runStart = -1;
|
|
260
|
+
runLength = 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const rendered = groups.map((g) => g.toString(16));
|
|
264
|
+
if (bestLength < 2) {
|
|
265
|
+
return rendered.join(':');
|
|
266
|
+
}
|
|
267
|
+
const head = rendered.slice(0, bestStart).join(':');
|
|
268
|
+
const tail = rendered.slice(bestStart + bestLength).join(':');
|
|
269
|
+
return `${head}::${tail}`;
|
|
270
|
+
}
|
|
271
|
+
function classifyIpv4Value(address) {
|
|
272
|
+
for (const range of IPV4_RANGES) {
|
|
273
|
+
const mask = (0xffffffff << (32 - range.prefixLength)) >>> 0;
|
|
274
|
+
if ((address & mask) >>> 0 === range.network) {
|
|
275
|
+
return range.classification;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return 'public';
|
|
279
|
+
}
|
|
280
|
+
function matchesIpv6Range(bytes, range) {
|
|
281
|
+
const fullBytes = range.prefixLength >>> 3;
|
|
282
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
283
|
+
if (bytes[i] !== range.prefix[i]) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const remainingBits = range.prefixLength & 7;
|
|
288
|
+
if (remainingBits === 0) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const mask = (0xff << (8 - remainingBits)) & 0xff;
|
|
292
|
+
return (bytes[fullBytes] & mask) === (range.prefix[fullBytes] & mask);
|
|
293
|
+
}
|
|
294
|
+
function allZero(bytes, start, end) {
|
|
295
|
+
for (let i = start; i < end; i++) {
|
|
296
|
+
if (bytes[i] !== 0) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
function readIpv4At(bytes, offset) {
|
|
303
|
+
return (((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Detects the IPv4 address embedded in an IPv6 address, if any. `::` and `::1`
|
|
307
|
+
* are excluded before the IPv4-compatible check so that the unspecified and
|
|
308
|
+
* loopback addresses keep their own classifications.
|
|
309
|
+
*/
|
|
310
|
+
function detectIpv6Embedding(bytes) {
|
|
311
|
+
if (allZero(bytes, 0, 10) && bytes[10] === 0xff && bytes[11] === 0xff) {
|
|
312
|
+
return { kind: 'ipv4-mapped', value: readIpv4At(bytes, 12) };
|
|
313
|
+
}
|
|
314
|
+
if (bytes[0] === 0x00 &&
|
|
315
|
+
bytes[1] === 0x64 &&
|
|
316
|
+
bytes[2] === 0xff &&
|
|
317
|
+
bytes[3] === 0x9b &&
|
|
318
|
+
allZero(bytes, 4, 12)) {
|
|
319
|
+
return { kind: 'nat64', value: readIpv4At(bytes, 12) };
|
|
320
|
+
}
|
|
321
|
+
if (bytes[0] === 0x20 && bytes[1] === 0x02) {
|
|
322
|
+
return { kind: '6to4', value: readIpv4At(bytes, 2) };
|
|
323
|
+
}
|
|
324
|
+
if (allZero(bytes, 0, 12)) {
|
|
325
|
+
return { kind: 'ipv4-compatible', value: readIpv4At(bytes, 12) };
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
function classifyIpv6Bytes(bytes) {
|
|
330
|
+
const canonical = ipv6ToString(bytes);
|
|
331
|
+
if (allZero(bytes, 0, 16)) {
|
|
332
|
+
return { address: canonical, canonical, family: 'ipv6', classification: 'unspecified' };
|
|
333
|
+
}
|
|
334
|
+
if (allZero(bytes, 0, 15) && bytes[15] === 0x01) {
|
|
335
|
+
return { address: canonical, canonical, family: 'ipv6', classification: 'loopback' };
|
|
336
|
+
}
|
|
337
|
+
const embedding = detectIpv6Embedding(bytes);
|
|
338
|
+
if (embedding !== undefined) {
|
|
339
|
+
return {
|
|
340
|
+
address: canonical,
|
|
341
|
+
canonical,
|
|
342
|
+
family: 'ipv6',
|
|
343
|
+
classification: classifyIpv4Value(embedding.value),
|
|
344
|
+
embeddedIpv4: { kind: embedding.kind, address: ipv4ToString(embedding.value) }
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
for (const range of IPV6_RANGES) {
|
|
348
|
+
if (matchesIpv6Range(bytes, range)) {
|
|
349
|
+
return { address: canonical, canonical, family: 'ipv6', classification: range.classification };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Only `2000::/3` is assigned as global unicast. Everything else that reached
|
|
353
|
+
// this point is unassigned or reserved for future use, so it is never public.
|
|
354
|
+
const classification = (bytes[0] & IPV6_GLOBAL_UNICAST_MASK) === IPV6_GLOBAL_UNICAST_VALUE ? 'public' : 'reserved';
|
|
355
|
+
return { address: canonical, canonical, family: 'ipv6', classification };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Normalizes the textual forms an address can arrive in before parsing:
|
|
359
|
+
* surrounding whitespace and the square brackets a URL `hostname` carries for
|
|
360
|
+
* an IPv6 literal. A zone identifier is *not* stripped here — it is stripped on
|
|
361
|
+
* the IPv6 path only, because `8.8.8.8%something` is not an address with a zone
|
|
362
|
+
* and must fail rather than be read as `8.8.8.8`.
|
|
363
|
+
*/
|
|
364
|
+
function normalizeAddressText(address) {
|
|
365
|
+
const text = address.trim();
|
|
366
|
+
if (text.length > 1 && text[0] === '[' && text[text.length - 1] === ']') {
|
|
367
|
+
return text.slice(1, -1);
|
|
368
|
+
}
|
|
369
|
+
return text;
|
|
370
|
+
}
|
|
371
|
+
/** Strips an IPv6 zone identifier (`fe80::1%eth0`). */
|
|
372
|
+
function stripIpv6Zone(text) {
|
|
373
|
+
const zone = text.indexOf('%');
|
|
374
|
+
return zone >= 0 ? text.slice(0, zone) : text;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Classifies a single IP address literal.
|
|
378
|
+
*
|
|
379
|
+
* The function is pure, synchronous, and deterministic — it performs no name
|
|
380
|
+
* resolution and no I/O. Input that is not an IP address literal (a DNS
|
|
381
|
+
* hostname, for instance) fails; callers that accept hostnames should treat a
|
|
382
|
+
* failure here as "not a literal" and resolve the name, then classify each
|
|
383
|
+
* resolved address.
|
|
384
|
+
*
|
|
385
|
+
* Accepted forms:
|
|
386
|
+
*
|
|
387
|
+
* - dotted-quad IPv4 (`169.254.169.254`)
|
|
388
|
+
* - the shortened and non-decimal IPv4 forms the WHATWG URL parser accepts
|
|
389
|
+
* (`127.1`, `2130706433`, `0177.0.0.1`, `0x7f.1`)
|
|
390
|
+
* - IPv6, with or without `::` compression, optionally bracketed as a URL
|
|
391
|
+
* `hostname` is (`[::1]`) and optionally carrying a zone id (`fe80::1%eth0`)
|
|
392
|
+
* - IPv6 forms that embed an IPv4 address — IPv4-mapped, IPv4-compatible,
|
|
393
|
+
* NAT64 and 6to4 — which are classified by their embedded IPv4 address
|
|
394
|
+
*
|
|
395
|
+
* A trailing dot is accepted on an IPv4 literal (`127.0.0.1.`), matching URL
|
|
396
|
+
* hostname normalization.
|
|
397
|
+
*
|
|
398
|
+
* **Classify a URL's `hostname`, never the raw URL text.** This function does
|
|
399
|
+
* not apply IDNA/Unicode normalization, and that step is not cosmetic: the
|
|
400
|
+
* platform's URL parser reads `http://127.0.0.1/`, `http://127。0。0。1/` and
|
|
401
|
+
* even `http://⑫7.0.0.1/` as the host `127.0.0.1`, because IDNA normalizes
|
|
402
|
+
* fullwidth digits, the ideographic full stop and circled numbers to their
|
|
403
|
+
* ASCII equivalents. Handed one of those strings directly this function fails —
|
|
404
|
+
* which is fail-closed, but only because the caller is then expected to treat
|
|
405
|
+
* "not a literal" as "resolve it as a name". Reading `new URL(...).hostname`
|
|
406
|
+
* gets the normalization for free and is the only supported use.
|
|
407
|
+
*
|
|
408
|
+
* @param address - the address literal to classify.
|
|
409
|
+
* @returns `Success` with the {@link IClassifiedAddress | classification},
|
|
410
|
+
* or `Failure` if the supplied text is not a well-formed IP address literal.
|
|
411
|
+
* @public
|
|
412
|
+
*/
|
|
413
|
+
export function classifyAddress(address) {
|
|
414
|
+
const text = normalizeAddressText(address);
|
|
415
|
+
if (text.length === 0) {
|
|
416
|
+
return fail(`"${address}": not a valid IP address literal (empty)`);
|
|
417
|
+
}
|
|
418
|
+
if (text.indexOf(':') >= 0) {
|
|
419
|
+
const bytes = parseIpv6(stripIpv6Zone(text));
|
|
420
|
+
if (bytes === undefined) {
|
|
421
|
+
return fail(`"${address}": not a valid IPv6 address literal`);
|
|
422
|
+
}
|
|
423
|
+
return succeed(Object.assign(Object.assign({}, classifyIpv6Bytes(bytes)), { address }));
|
|
424
|
+
}
|
|
425
|
+
const v4Text = text.length > 1 && text[text.length - 1] === '.' ? text.slice(0, -1) : text;
|
|
426
|
+
const value = parseIpv4(v4Text);
|
|
427
|
+
if (value === undefined) {
|
|
428
|
+
return fail(`"${address}": not a valid IP address literal`);
|
|
429
|
+
}
|
|
430
|
+
const canonical = ipv4ToString(value);
|
|
431
|
+
return succeed({
|
|
432
|
+
address,
|
|
433
|
+
canonical,
|
|
434
|
+
family: 'ipv4',
|
|
435
|
+
classification: classifyIpv4Value(value)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=addressClassification.js.map
|