@uncefact/untp-utils 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -674
- package/README.md +23 -1
- package/build/artefacts/detect-version-from-context.d.ts +46 -0
- package/build/artefacts/detect-version-from-context.js +77 -0
- package/build/artefacts/detect-version-from-context.js.map +1 -0
- package/build/artefacts/detect-version-from-context.test.d.ts +1 -0
- package/build/artefacts/detect-version-from-context.test.js +69 -0
- package/build/artefacts/detect-version-from-context.test.js.map +1 -0
- package/build/artefacts/index.d.ts +3 -0
- package/build/artefacts/index.js +4 -0
- package/build/artefacts/index.js.map +1 -0
- package/build/artefacts/urls.d.ts +79 -0
- package/build/artefacts/urls.js +133 -0
- package/build/artefacts/urls.js.map +1 -0
- package/build/artefacts/urls.test.d.ts +1 -0
- package/build/artefacts/urls.test.js +91 -0
- package/build/artefacts/urls.test.js.map +1 -0
- package/build/artefacts/version.d.ts +9 -0
- package/build/artefacts/version.js +17 -0
- package/build/artefacts/version.js.map +1 -0
- package/build/artefacts/version.test.d.ts +1 -0
- package/build/artefacts/version.test.js +16 -0
- package/build/artefacts/version.test.js.map +1 -0
- package/build/cache/in-memory-ttl-cache.d.ts +2 -0
- package/build/cache/in-memory-ttl-cache.js +76 -0
- package/build/cache/in-memory-ttl-cache.js.map +1 -0
- package/build/cache/in-memory-ttl-cache.test.d.ts +1 -0
- package/build/cache/in-memory-ttl-cache.test.js +236 -0
- package/build/cache/in-memory-ttl-cache.test.js.map +1 -0
- package/build/cache/index.d.ts +2 -0
- package/build/cache/index.js +2 -0
- package/build/cache/index.js.map +1 -0
- package/build/cache/ttl-cache.d.ts +22 -0
- package/build/cache/ttl-cache.js +2 -0
- package/build/cache/ttl-cache.js.map +1 -0
- package/build/common/as-non-empty-string.d.ts +7 -0
- package/build/common/as-non-empty-string.js +10 -0
- package/build/common/as-non-empty-string.js.map +1 -0
- package/build/common/as-non-empty-string.test.d.ts +1 -0
- package/build/common/as-non-empty-string.test.js +25 -0
- package/build/common/as-non-empty-string.test.js.map +1 -0
- package/build/common/require-string.d.ts +12 -0
- package/build/common/require-string.js +27 -0
- package/build/common/require-string.js.map +1 -0
- package/build/common/require-string.test.d.ts +1 -0
- package/build/common/require-string.test.js +53 -0
- package/build/common/require-string.test.js.map +1 -0
- package/build/conformity-vocabulary/codes.d.ts +10 -0
- package/build/conformity-vocabulary/codes.js +10 -0
- package/build/conformity-vocabulary/codes.js.map +1 -0
- package/build/conformity-vocabulary/errors.d.ts +44 -0
- package/build/conformity-vocabulary/errors.js +68 -0
- package/build/conformity-vocabulary/errors.js.map +1 -0
- package/build/conformity-vocabulary/index.d.ts +6 -0
- package/build/conformity-vocabulary/index.js +7 -0
- package/build/conformity-vocabulary/index.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.d.ts +26 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.js +106 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.d.ts +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.js +327 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.d.ts +28 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.js +47 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.d.ts +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.js +284 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.js.map +1 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.d.ts +19 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.js +219 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.js.map +1 -0
- package/build/conformity-vocabulary/types.d.ts +180 -0
- package/build/conformity-vocabulary/types.js +2 -0
- package/build/conformity-vocabulary/types.js.map +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.d.ts +26 -0
- package/build/conformity-vocabulary/validate-conformity-claim.js +165 -0
- package/build/conformity-vocabulary/validate-conformity-claim.js.map +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.d.ts +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.js +478 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.js.map +1 -0
- package/build/http-headers/index.d.ts +5 -0
- package/build/http-headers/index.js +6 -0
- package/build/http-headers/index.js.map +1 -0
- package/build/http-headers/is-safe-header-value.d.ts +10 -0
- package/build/http-headers/is-safe-header-value.js +15 -0
- package/build/http-headers/is-safe-header-value.js.map +1 -0
- package/build/http-headers/is-safe-header-value.test.d.ts +1 -0
- package/build/http-headers/is-safe-header-value.test.js +37 -0
- package/build/http-headers/is-safe-header-value.test.js.map +1 -0
- package/build/http-headers/parse-entity-tag.d.ts +9 -0
- package/build/http-headers/parse-entity-tag.js +18 -0
- package/build/http-headers/parse-entity-tag.js.map +1 -0
- package/build/http-headers/parse-entity-tag.test.d.ts +1 -0
- package/build/http-headers/parse-entity-tag.test.js +41 -0
- package/build/http-headers/parse-entity-tag.test.js.map +1 -0
- package/build/http-headers/parse-imf-date.d.ts +13 -0
- package/build/http-headers/parse-imf-date.js +23 -0
- package/build/http-headers/parse-imf-date.js.map +1 -0
- package/build/http-headers/parse-imf-date.test.d.ts +1 -0
- package/build/http-headers/parse-imf-date.test.js +31 -0
- package/build/http-headers/parse-imf-date.test.js.map +1 -0
- package/build/http-headers/parse-media-type.d.ts +8 -0
- package/build/http-headers/parse-media-type.js +21 -0
- package/build/http-headers/parse-media-type.js.map +1 -0
- package/build/http-headers/parse-media-type.test.d.ts +1 -0
- package/build/http-headers/parse-media-type.test.js +39 -0
- package/build/http-headers/parse-media-type.test.js.map +1 -0
- package/build/http-headers/user-agent.d.ts +35 -0
- package/build/http-headers/user-agent.js +45 -0
- package/build/http-headers/user-agent.js.map +1 -0
- package/build/http-headers/user-agent.test.d.ts +1 -0
- package/build/http-headers/user-agent.test.js +27 -0
- package/build/http-headers/user-agent.test.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/loaders/errors.d.ts +21 -0
- package/build/loaders/errors.js +45 -0
- package/build/loaders/errors.js.map +1 -0
- package/build/loaders/index.d.ts +3 -0
- package/build/loaders/index.js +4 -0
- package/build/loaders/index.js.map +1 -0
- package/build/loaders/jsonld-document-loader.d.ts +46 -0
- package/build/loaders/jsonld-document-loader.js +34 -0
- package/build/loaders/jsonld-document-loader.js.map +1 -0
- package/build/loaders/jsonld-document-loader.test.d.ts +1 -0
- package/build/loaders/jsonld-document-loader.test.js +71 -0
- package/build/loaders/jsonld-document-loader.test.js.map +1 -0
- package/build/loaders/schema-loader.d.ts +21 -0
- package/build/loaders/schema-loader.js +67 -0
- package/build/loaders/schema-loader.js.map +1 -0
- package/build/loaders/schema-loader.ssrf.test.d.ts +1 -0
- package/build/loaders/schema-loader.ssrf.test.js +55 -0
- package/build/loaders/schema-loader.ssrf.test.js.map +1 -0
- package/build/loaders/schema-loader.test.d.ts +1 -0
- package/build/loaders/schema-loader.test.js +162 -0
- package/build/loaders/schema-loader.test.js.map +1 -0
- package/build/multibase-digest/multibase-digest.d.ts +23 -0
- package/build/multibase-digest/multibase-digest.js +40 -0
- package/build/multibase-digest/multibase-digest.js.map +1 -1
- package/build/multibase-digest/multibase-digest.test.js +46 -0
- package/build/multibase-digest/multibase-digest.test.js.map +1 -1
- package/build/node/errors.d.ts +41 -0
- package/build/node/errors.js +80 -0
- package/build/node/errors.js.map +1 -0
- package/build/node/index.d.ts +3 -0
- package/build/node/index.js +4 -0
- package/build/node/index.js.map +1 -0
- package/build/node/is-private-ip.d.ts +41 -0
- package/build/node/is-private-ip.js +138 -0
- package/build/node/is-private-ip.js.map +1 -0
- package/build/node/is-private-ip.test.d.ts +1 -0
- package/build/node/is-private-ip.test.js +96 -0
- package/build/node/is-private-ip.test.js.map +1 -0
- package/build/node/validate-public-url.d.ts +49 -0
- package/build/node/validate-public-url.js +94 -0
- package/build/node/validate-public-url.js.map +1 -0
- package/build/node/validate-public-url.test.d.ts +1 -0
- package/build/node/validate-public-url.test.js +177 -0
- package/build/node/validate-public-url.test.js.map +1 -0
- package/build/resolvers/errors.d.ts +43 -0
- package/build/resolvers/errors.js +99 -0
- package/build/resolvers/errors.js.map +1 -0
- package/build/resolvers/index.d.ts +5 -0
- package/build/resolvers/index.js +6 -0
- package/build/resolvers/index.js.map +1 -0
- package/build/resolvers/resolve-document-if-changed.d.ts +48 -0
- package/build/resolvers/resolve-document-if-changed.js +42 -0
- package/build/resolvers/resolve-document-if-changed.js.map +1 -0
- package/build/resolvers/resolve-document-if-changed.test.d.ts +1 -0
- package/build/resolvers/resolve-document-if-changed.test.js +109 -0
- package/build/resolvers/resolve-document-if-changed.test.js.map +1 -0
- package/build/resolvers/resolve-document.d.ts +86 -0
- package/build/resolvers/resolve-document.js +239 -0
- package/build/resolvers/resolve-document.js.map +1 -0
- package/build/resolvers/resolve-document.test.d.ts +1 -0
- package/build/resolvers/resolve-document.test.js +299 -0
- package/build/resolvers/resolve-document.test.js.map +1 -0
- package/build/resolvers/resolve-json-document.d.ts +40 -0
- package/build/resolvers/resolve-json-document.js +42 -0
- package/build/resolvers/resolve-json-document.js.map +1 -0
- package/build/resolvers/resolve-json-document.test.d.ts +1 -0
- package/build/resolvers/resolve-json-document.test.js +61 -0
- package/build/resolvers/resolve-json-document.test.js.map +1 -0
- package/build/structured-error.d.ts +49 -0
- package/build/structured-error.js +17 -0
- package/build/structured-error.js.map +1 -0
- package/build/structured-error.test.d.ts +1 -0
- package/build/structured-error.test.js +112 -0
- package/build/structured-error.test.js.map +1 -0
- package/build/validation/describe-jsonld-failure.d.ts +50 -0
- package/build/validation/describe-jsonld-failure.js +104 -0
- package/build/validation/describe-jsonld-failure.js.map +1 -0
- package/build/validation/describe-jsonld-failure.test.d.ts +1 -0
- package/build/validation/describe-jsonld-failure.test.js +141 -0
- package/build/validation/describe-jsonld-failure.test.js.map +1 -0
- package/build/validation/errors.d.ts +40 -0
- package/build/validation/errors.js +75 -0
- package/build/validation/errors.js.map +1 -0
- package/build/validation/index.d.ts +4 -0
- package/build/validation/index.js +5 -0
- package/build/validation/index.js.map +1 -0
- package/build/validation/validate-against-schemas.d.ts +22 -0
- package/build/validation/validate-against-schemas.js +65 -0
- package/build/validation/validate-against-schemas.js.map +1 -0
- package/build/validation/validate-against-schemas.test.d.ts +1 -0
- package/build/validation/validate-against-schemas.test.js +169 -0
- package/build/validation/validate-against-schemas.test.js.map +1 -0
- package/build/validation/validate-jsonld.context-cache.test.d.ts +1 -0
- package/build/validation/validate-jsonld.context-cache.test.js +32 -0
- package/build/validation/validate-jsonld.context-cache.test.js.map +1 -0
- package/build/validation/validate-jsonld.d.ts +33 -0
- package/build/validation/validate-jsonld.js +99 -0
- package/build/validation/validate-jsonld.js.map +1 -0
- package/build/validation/validate-jsonld.ssrf.test.d.ts +1 -0
- package/build/validation/validate-jsonld.ssrf.test.js +108 -0
- package/build/validation/validate-jsonld.ssrf.test.js.map +1 -0
- package/build/validation/validate-jsonld.test.d.ts +1 -0
- package/build/validation/validate-jsonld.test.js +200 -0
- package/build/validation/validate-jsonld.test.js.map +1 -0
- package/package.json +77 -11
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { InvalidUrlError, PrivateAddressError, PrivateHostnameError, ResolutionEmptyError, ResolutionFailedError, UrlValidationError, } from './errors.js';
|
|
3
|
+
const lookup = jest.fn();
|
|
4
|
+
jest.unstable_mockModule('node:dns/promises', () => ({
|
|
5
|
+
default: { lookup },
|
|
6
|
+
lookup,
|
|
7
|
+
}));
|
|
8
|
+
const { validatePublicUrl } = await import('./validate-public-url.js');
|
|
9
|
+
describe('validatePublicUrl', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
lookup.mockReset();
|
|
12
|
+
});
|
|
13
|
+
describe('URL parsing', () => {
|
|
14
|
+
it('throws InvalidUrlError when the string is not a parseable URL', async () => {
|
|
15
|
+
await expect(validatePublicUrl('not a url')).rejects.toBeInstanceOf(InvalidUrlError);
|
|
16
|
+
await expect(validatePublicUrl('not a url')).rejects.toMatchObject({
|
|
17
|
+
code: 'url.invalid',
|
|
18
|
+
received: 'not a url',
|
|
19
|
+
});
|
|
20
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('scheme', () => {
|
|
24
|
+
it('throws UnsupportedSchemeError for an unallowed scheme', async () => {
|
|
25
|
+
await expect(validatePublicUrl('ftp://example.com/path')).rejects.toMatchObject({
|
|
26
|
+
name: 'UnsupportedSchemeError',
|
|
27
|
+
code: 'url.unsupported-scheme',
|
|
28
|
+
received: 'ftp',
|
|
29
|
+
});
|
|
30
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
it('accepts a caller-supplied allowedSchemes list', async () => {
|
|
33
|
+
lookup.mockResolvedValue([{ address: '1.1.1.1', family: 4 }]);
|
|
34
|
+
await expect(validatePublicUrl('wss://example.com/ws', { allowedSchemes: ['wss', 'ws'] })).resolves.toEqual({
|
|
35
|
+
address: '1.1.1.1',
|
|
36
|
+
family: 4,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('reports canonical scheme details on the error', async () => {
|
|
40
|
+
await expect(validatePublicUrl('ftp://example.com/')).rejects.toMatchObject({
|
|
41
|
+
received: 'ftp',
|
|
42
|
+
expected: ['http', 'https'],
|
|
43
|
+
remediation: 'Use one of: http, https.',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('compares schemes case-insensitively', async () => {
|
|
47
|
+
lookup.mockResolvedValue([{ address: '1.1.1.1', family: 4 }]);
|
|
48
|
+
await expect(validatePublicUrl('HTTPS://example.com/')).resolves.toEqual({
|
|
49
|
+
address: '1.1.1.1',
|
|
50
|
+
family: 4,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('hostname', () => {
|
|
55
|
+
it('rejects localhost without DNS lookup', async () => {
|
|
56
|
+
await expect(validatePublicUrl('http://localhost/path')).rejects.toBeInstanceOf(PrivateHostnameError);
|
|
57
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
it('rejects .internal suffix without DNS lookup', async () => {
|
|
60
|
+
await expect(validatePublicUrl('http://db.internal/')).rejects.toBeInstanceOf(PrivateHostnameError);
|
|
61
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('rejects a literal private IPv4 in the URL without DNS lookup', async () => {
|
|
64
|
+
await expect(validatePublicUrl('http://10.0.0.1/')).rejects.toBeInstanceOf(PrivateHostnameError);
|
|
65
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it('rejects a literal IPv6 loopback in the URL without DNS lookup', async () => {
|
|
68
|
+
await expect(validatePublicUrl('http://[::1]/')).rejects.toBeInstanceOf(PrivateHostnameError);
|
|
69
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
it('returns the literal IP for a public IPv4 host without calling DNS', async () => {
|
|
72
|
+
await expect(validatePublicUrl('http://1.1.1.1/')).resolves.toEqual({ address: '1.1.1.1', family: 4 });
|
|
73
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
it('returns the literal IP for a public IPv6 host without calling DNS', async () => {
|
|
76
|
+
await expect(validatePublicUrl('http://[2606:4700:4700::1111]/')).resolves.toEqual({
|
|
77
|
+
address: '2606:4700:4700::1111',
|
|
78
|
+
family: 6,
|
|
79
|
+
});
|
|
80
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
it('rejects a literal 0.0.0.0 host without DNS lookup', async () => {
|
|
83
|
+
await expect(validatePublicUrl('http://0.0.0.0/')).rejects.toBeInstanceOf(PrivateHostnameError);
|
|
84
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
it('rejects userinfo@host smuggling where the host is private', async () => {
|
|
87
|
+
// `URL` discards the `evil.com@` userinfo; pin the regression.
|
|
88
|
+
await expect(validatePublicUrl('http://evil.com@127.0.0.1/')).rejects.toMatchObject({
|
|
89
|
+
name: 'PrivateHostnameError',
|
|
90
|
+
received: '127.0.0.1',
|
|
91
|
+
});
|
|
92
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('DNS resolution', () => {
|
|
96
|
+
it('throws ResolutionFailedError when dns.lookup rejects', async () => {
|
|
97
|
+
lookup.mockRejectedValue(new Error('ENOTFOUND example.com'));
|
|
98
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toMatchObject({
|
|
99
|
+
name: 'ResolutionFailedError',
|
|
100
|
+
code: 'url.resolution-failed',
|
|
101
|
+
received: 'ENOTFOUND example.com',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('throws ResolutionEmptyError when the resolver returns no records', async () => {
|
|
105
|
+
lookup.mockResolvedValue([]);
|
|
106
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(ResolutionEmptyError);
|
|
107
|
+
});
|
|
108
|
+
it('passes the family option through to dns.lookup with all: true', async () => {
|
|
109
|
+
lookup.mockResolvedValue([{ address: '1.1.1.1', family: 4 }]);
|
|
110
|
+
await validatePublicUrl('https://example.com/', { family: 4 });
|
|
111
|
+
expect(lookup).toHaveBeenCalledWith('example.com', { family: 4, all: true });
|
|
112
|
+
});
|
|
113
|
+
it('defaults the family option to 0 (any)', async () => {
|
|
114
|
+
lookup.mockResolvedValue([{ address: '1.1.1.1', family: 4 }]);
|
|
115
|
+
await validatePublicUrl('https://example.com/');
|
|
116
|
+
expect(lookup).toHaveBeenCalledWith('example.com', { family: 0, all: true });
|
|
117
|
+
});
|
|
118
|
+
it('throws ResolutionFailedError when dns returns an unsupported family', async () => {
|
|
119
|
+
lookup.mockResolvedValue([{ address: 'whatever', family: 7 }]);
|
|
120
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(ResolutionFailedError);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('rebind defence', () => {
|
|
124
|
+
it('rejects a hostname that resolves to AWS metadata', async () => {
|
|
125
|
+
lookup.mockResolvedValue([{ address: '169.254.169.254', family: 4 }]);
|
|
126
|
+
await expect(validatePublicUrl('https://metadata.attacker.example/')).rejects.toMatchObject({
|
|
127
|
+
name: 'PrivateAddressError',
|
|
128
|
+
code: 'url.private-address',
|
|
129
|
+
resolvedAddresses: ['169.254.169.254'],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
it('rejects a hostname that resolves to an RFC 1918 address', async () => {
|
|
133
|
+
lookup.mockResolvedValue([{ address: '10.0.0.1', family: 4 }]);
|
|
134
|
+
await expect(validatePublicUrl('https://internal.attacker.example/')).rejects.toMatchObject({
|
|
135
|
+
name: 'PrivateAddressError',
|
|
136
|
+
resolvedAddresses: ['10.0.0.1'],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('rejects a hostname that resolves to an IPv6 loopback', async () => {
|
|
140
|
+
lookup.mockResolvedValue([{ address: '::1', family: 6 }]);
|
|
141
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(PrivateAddressError);
|
|
142
|
+
});
|
|
143
|
+
it('rejects a hostname that resolves to an IPv6 unique-local address', async () => {
|
|
144
|
+
lookup.mockResolvedValue([{ address: 'fc00::1', family: 6 }]);
|
|
145
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(PrivateAddressError);
|
|
146
|
+
});
|
|
147
|
+
it('rejects a hostname that resolves to an IPv4-mapped private IPv6 address', async () => {
|
|
148
|
+
lookup.mockResolvedValue([{ address: '::ffff:10.0.0.1', family: 6 }]);
|
|
149
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(PrivateAddressError);
|
|
150
|
+
});
|
|
151
|
+
it('rejects when any A or AAAA record is private even if others are public', async () => {
|
|
152
|
+
lookup.mockResolvedValue([
|
|
153
|
+
{ address: '1.1.1.1', family: 4 },
|
|
154
|
+
{ address: '169.254.169.254', family: 4 },
|
|
155
|
+
]);
|
|
156
|
+
await expect(validatePublicUrl('https://mixed.attacker.example/')).rejects.toMatchObject({
|
|
157
|
+
name: 'PrivateAddressError',
|
|
158
|
+
resolvedAddresses: ['169.254.169.254'],
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it('returns the first resolved IP so callers can use it as the connect target', async () => {
|
|
162
|
+
lookup.mockResolvedValue([
|
|
163
|
+
{ address: '1.1.1.1', family: 4 },
|
|
164
|
+
{ address: '8.8.8.8', family: 4 },
|
|
165
|
+
]);
|
|
166
|
+
await expect(validatePublicUrl('https://example.com/')).resolves.toEqual({ address: '1.1.1.1', family: 4 });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('hierarchy', () => {
|
|
170
|
+
it('every concrete error extends UrlValidationError', async () => {
|
|
171
|
+
await expect(validatePublicUrl('not a url')).rejects.toBeInstanceOf(UrlValidationError);
|
|
172
|
+
lookup.mockResolvedValue([{ address: '10.0.0.1', family: 4 }]);
|
|
173
|
+
await expect(validatePublicUrl('https://example.com/')).rejects.toBeInstanceOf(UrlValidationError);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=validate-public-url.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-public-url.test.js","sourceRoot":"","sources":["../../src/node/validate-public-url.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StructuredError } from '../structured-error.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base for every diagnostic from `@uncefact/untp-utils/resolvers`.
|
|
4
|
+
* Catch to handle any resolver failure generically; catch a concrete
|
|
5
|
+
* subclass for specific handling. URL-shape failures (invalid URL,
|
|
6
|
+
* unsupported scheme, private hostname, resolution failure, private
|
|
7
|
+
* address) surface as `UrlValidationError` subclasses from the
|
|
8
|
+
* underlying `validatePublicUrl` call rather than being re-wrapped here.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ResolverError extends StructuredError {
|
|
11
|
+
}
|
|
12
|
+
/** The fetch call rejected before producing a response (TCP/TLS error, DNS race, etc.). */
|
|
13
|
+
export declare class ResolverNetworkError extends ResolverError {
|
|
14
|
+
constructor(url: string, cause: unknown);
|
|
15
|
+
}
|
|
16
|
+
/** The remote returned a non-success HTTP status. */
|
|
17
|
+
export declare class ResolverHttpError extends ResolverError {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
constructor(url: string, status: number);
|
|
20
|
+
}
|
|
21
|
+
/** The response body exceeded the configured size cap. */
|
|
22
|
+
export declare class ResolverTooLargeError extends ResolverError {
|
|
23
|
+
readonly limit: number;
|
|
24
|
+
constructor(url: string, limit: number);
|
|
25
|
+
}
|
|
26
|
+
/** A redirect chain exceeded the configured maximum hop count. */
|
|
27
|
+
export declare class ResolverTooManyRedirectsError extends ResolverError {
|
|
28
|
+
readonly limit: number;
|
|
29
|
+
constructor(url: string, limit: number);
|
|
30
|
+
}
|
|
31
|
+
/** The fetch was aborted by the timeout signal. */
|
|
32
|
+
export declare class ResolverTimedOutError extends ResolverError {
|
|
33
|
+
readonly timeoutMs: number;
|
|
34
|
+
constructor(url: string, timeoutMs: number, cause?: unknown);
|
|
35
|
+
}
|
|
36
|
+
/** A 3xx response was received without a parseable `Location` header. */
|
|
37
|
+
export declare class ResolverRedirectMissingLocationError extends ResolverError {
|
|
38
|
+
constructor(url: string, received: string | number, cause?: unknown);
|
|
39
|
+
}
|
|
40
|
+
/** The response body was fetched but could not be parsed as JSON. */
|
|
41
|
+
export declare class ResolverInvalidJsonError extends ResolverError {
|
|
42
|
+
constructor(url: string, cause: unknown);
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { StructuredError } from '../structured-error.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base for every diagnostic from `@uncefact/untp-utils/resolvers`.
|
|
4
|
+
* Catch to handle any resolver failure generically; catch a concrete
|
|
5
|
+
* subclass for specific handling. URL-shape failures (invalid URL,
|
|
6
|
+
* unsupported scheme, private hostname, resolution failure, private
|
|
7
|
+
* address) surface as `UrlValidationError` subclasses from the
|
|
8
|
+
* underlying `validatePublicUrl` call rather than being re-wrapped here.
|
|
9
|
+
*/
|
|
10
|
+
export class ResolverError extends StructuredError {
|
|
11
|
+
}
|
|
12
|
+
/** The fetch call rejected before producing a response (TCP/TLS error, DNS race, etc.). */
|
|
13
|
+
export class ResolverNetworkError extends ResolverError {
|
|
14
|
+
constructor(url, cause) {
|
|
15
|
+
super({
|
|
16
|
+
code: 'resolver.network-error',
|
|
17
|
+
message: `Network error fetching ${url}.`,
|
|
18
|
+
received: cause instanceof Error ? cause.message : String(cause),
|
|
19
|
+
cause,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The remote returned a non-success HTTP status. */
|
|
24
|
+
export class ResolverHttpError extends ResolverError {
|
|
25
|
+
status;
|
|
26
|
+
constructor(url, status) {
|
|
27
|
+
super({
|
|
28
|
+
code: 'resolver.http-error',
|
|
29
|
+
message: `${url} returned status ${status}.`,
|
|
30
|
+
received: status,
|
|
31
|
+
expected: '2xx',
|
|
32
|
+
});
|
|
33
|
+
this.status = status;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The response body exceeded the configured size cap. */
|
|
37
|
+
export class ResolverTooLargeError extends ResolverError {
|
|
38
|
+
limit;
|
|
39
|
+
constructor(url, limit) {
|
|
40
|
+
super({
|
|
41
|
+
code: 'resolver.too-large',
|
|
42
|
+
message: `Response body for ${url} exceeds ${limit}-byte limit.`,
|
|
43
|
+
received: `> ${limit} bytes`,
|
|
44
|
+
expected: `<= ${limit} bytes`,
|
|
45
|
+
});
|
|
46
|
+
this.limit = limit;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A redirect chain exceeded the configured maximum hop count. */
|
|
50
|
+
export class ResolverTooManyRedirectsError extends ResolverError {
|
|
51
|
+
limit;
|
|
52
|
+
constructor(url, limit) {
|
|
53
|
+
super({
|
|
54
|
+
code: 'resolver.too-many-redirects',
|
|
55
|
+
message: `Exceeded ${limit} redirect hops starting from ${url}.`,
|
|
56
|
+
received: `> ${limit} hops`,
|
|
57
|
+
expected: `<= ${limit} hops`,
|
|
58
|
+
});
|
|
59
|
+
this.limit = limit;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The fetch was aborted by the timeout signal. */
|
|
63
|
+
export class ResolverTimedOutError extends ResolverError {
|
|
64
|
+
timeoutMs;
|
|
65
|
+
constructor(url, timeoutMs, cause) {
|
|
66
|
+
super({
|
|
67
|
+
code: 'resolver.timed-out',
|
|
68
|
+
message: `Request to ${url} timed out after ${timeoutMs}ms.`,
|
|
69
|
+
received: `> ${timeoutMs}ms`,
|
|
70
|
+
expected: `<= ${timeoutMs}ms`,
|
|
71
|
+
cause,
|
|
72
|
+
});
|
|
73
|
+
this.timeoutMs = timeoutMs;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** A 3xx response was received without a parseable `Location` header. */
|
|
77
|
+
export class ResolverRedirectMissingLocationError extends ResolverError {
|
|
78
|
+
constructor(url, received, cause) {
|
|
79
|
+
super({
|
|
80
|
+
code: 'resolver.redirect-missing-location',
|
|
81
|
+
message: `Redirect from ${url} had no parseable Location header.`,
|
|
82
|
+
received,
|
|
83
|
+
cause,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The response body was fetched but could not be parsed as JSON. */
|
|
88
|
+
export class ResolverInvalidJsonError extends ResolverError {
|
|
89
|
+
constructor(url, cause) {
|
|
90
|
+
super({
|
|
91
|
+
code: 'resolver.invalid-json',
|
|
92
|
+
message: `Response body for ${url} is not valid JSON.`,
|
|
93
|
+
received: cause instanceof Error ? cause.message : String(cause),
|
|
94
|
+
expected: 'a JSON document',
|
|
95
|
+
cause,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/resolvers/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ResolverError, ResolverNetworkError, ResolverHttpError, ResolverTooLargeError, ResolverTooManyRedirectsError, ResolverTimedOutError, ResolverRedirectMissingLocationError, ResolverInvalidJsonError, } from './errors.js';
|
|
2
|
+
export { resolveDocument, RESOLVER_DEFAULTS, type LoadResult, type ResolveDocumentOptions, } from './resolve-document.js';
|
|
3
|
+
export { DEFAULT_USER_AGENT, USER_AGENT_ENV_VAR, isValidHttpUserAgent } from '../http-headers/index.js';
|
|
4
|
+
export { resolveJsonDocument, type ResolveJsonDocumentOptions, type ResolvedJsonDocument, } from './resolve-json-document.js';
|
|
5
|
+
export { resolveDocumentIfChanged, type CachedResource, type ResolveDocumentIfChangedValue, } from './resolve-document-if-changed.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ResolverError, ResolverNetworkError, ResolverHttpError, ResolverTooLargeError, ResolverTooManyRedirectsError, ResolverTimedOutError, ResolverRedirectMissingLocationError, ResolverInvalidJsonError, } from './errors.js';
|
|
2
|
+
export { resolveDocument, RESOLVER_DEFAULTS, } from './resolve-document.js';
|
|
3
|
+
export { DEFAULT_USER_AGENT, USER_AGENT_ENV_VAR, isValidHttpUserAgent } from '../http-headers/index.js';
|
|
4
|
+
export { resolveJsonDocument, } from './resolve-json-document.js';
|
|
5
|
+
export { resolveDocumentIfChanged, } from './resolve-document-if-changed.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,6BAA6B,EAC7B,qBAAqB,EACrB,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,iBAAiB,GAGlB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACxG,OAAO,EACL,mBAAmB,GAGpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,wBAAwB,GAGzB,MAAM,kCAAkC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MultibaseDigest } from '../multibase-digest/index.js';
|
|
2
|
+
import { type LoadResult, type ResolveDocumentOptions } from './resolve-document.js';
|
|
3
|
+
/**
|
|
4
|
+
* Description of a previously-cached resource, used by
|
|
5
|
+
* {@link resolveDocumentIfChanged} to drive the conditional-fetch skip
|
|
6
|
+
* chain (`If-None-Match`, `If-Modified-Since`, body-digest comparison).
|
|
7
|
+
*/
|
|
8
|
+
export interface CachedResource {
|
|
9
|
+
/** The `ETag` previously observed for this URL, if any. */
|
|
10
|
+
etag?: string;
|
|
11
|
+
/** The `Last-Modified` header previously observed, if any. */
|
|
12
|
+
lastModifiedHeader?: string;
|
|
13
|
+
/** The body digest of the previously cached response. */
|
|
14
|
+
bodyDigest?: MultibaseDigest;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Discriminated return from {@link resolveDocumentIfChanged}.
|
|
18
|
+
*
|
|
19
|
+
* - `{ kind: 'unchanged' }`: the upstream confirmed the cache is fresh
|
|
20
|
+
* (either via a `304 Not Modified` or via a `200` whose body digest
|
|
21
|
+
* matched `cached.bodyDigest`).
|
|
22
|
+
* - `{ kind: 'loaded'; result }`: the upstream returned fresh content;
|
|
23
|
+
* `result` is the new {@link LoadResult} the caller should store.
|
|
24
|
+
*/
|
|
25
|
+
export type ResolveDocumentIfChangedValue = {
|
|
26
|
+
kind: 'unchanged';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'loaded';
|
|
29
|
+
result: LoadResult;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Conditional-fetch wrapper around {@link resolveDocument} that implements
|
|
33
|
+
* the skip chain from ADR-033 §1.
|
|
34
|
+
*
|
|
35
|
+
* Three short-circuits, in order:
|
|
36
|
+
*
|
|
37
|
+
* 1. **`304 Not Modified`** from the upstream (driven by `If-None-Match`
|
|
38
|
+
* and `If-Modified-Since` headers built from `cached.etag` and
|
|
39
|
+
* `cached.lastModifiedHeader`).
|
|
40
|
+
* 2. **Body-digest match** after a `200 OK` (the upstream ignored the
|
|
41
|
+
* conditional headers but returned the same content; compared against
|
|
42
|
+
* `cached.bodyDigest`).
|
|
43
|
+
* 3. Otherwise, the fresh {@link LoadResult} is returned under the
|
|
44
|
+
* `loaded` discriminator.
|
|
45
|
+
*
|
|
46
|
+
* Errors from the underlying {@link resolveDocument} propagate unchanged.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveDocumentIfChanged(url: string, cached: CachedResource, options?: ResolveDocumentOptions): Promise<ResolveDocumentIfChangedValue>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolveDocument } from './resolve-document.js';
|
|
2
|
+
/**
|
|
3
|
+
* Conditional-fetch wrapper around {@link resolveDocument} that implements
|
|
4
|
+
* the skip chain from ADR-033 §1.
|
|
5
|
+
*
|
|
6
|
+
* Three short-circuits, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. **`304 Not Modified`** from the upstream (driven by `If-None-Match`
|
|
9
|
+
* and `If-Modified-Since` headers built from `cached.etag` and
|
|
10
|
+
* `cached.lastModifiedHeader`).
|
|
11
|
+
* 2. **Body-digest match** after a `200 OK` (the upstream ignored the
|
|
12
|
+
* conditional headers but returned the same content; compared against
|
|
13
|
+
* `cached.bodyDigest`).
|
|
14
|
+
* 3. Otherwise, the fresh {@link LoadResult} is returned under the
|
|
15
|
+
* `loaded` discriminator.
|
|
16
|
+
*
|
|
17
|
+
* Errors from the underlying {@link resolveDocument} propagate unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveDocumentIfChanged(url, cached, options) {
|
|
20
|
+
// HTTP header names are case-insensitive on the wire, but a plain
|
|
21
|
+
// `Record<string, string>` is not: a caller-supplied `'if-none-match'`
|
|
22
|
+
// and our `'If-None-Match'` would survive as two distinct keys and
|
|
23
|
+
// undici would emit both. Normalise every key to lower-case so the
|
|
24
|
+
// cached validator wins cleanly.
|
|
25
|
+
const conditionalHeaders = {};
|
|
26
|
+
for (const [key, value] of Object.entries(options?.headers ?? {})) {
|
|
27
|
+
conditionalHeaders[key.toLowerCase()] = value;
|
|
28
|
+
}
|
|
29
|
+
if (cached.etag)
|
|
30
|
+
conditionalHeaders['if-none-match'] = cached.etag;
|
|
31
|
+
if (cached.lastModifiedHeader)
|
|
32
|
+
conditionalHeaders['if-modified-since'] = cached.lastModifiedHeader;
|
|
33
|
+
const result = await resolveDocument(url, { ...options, headers: conditionalHeaders });
|
|
34
|
+
if (result.status === 304) {
|
|
35
|
+
return { kind: 'unchanged' };
|
|
36
|
+
}
|
|
37
|
+
if (cached.bodyDigest && result.bodyDigest.toString() === cached.bodyDigest.toString()) {
|
|
38
|
+
return { kind: 'unchanged' };
|
|
39
|
+
}
|
|
40
|
+
return { kind: 'loaded', result };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=resolve-document-if-changed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-document-if-changed.js","sourceRoot":"","sources":["../../src/resolvers/resolve-document-if-changed.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAgD,MAAM,uBAAuB,CAAC;AA2BtG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,GAAW,EACX,MAAsB,EACtB,OAAgC;IAEhC,kEAAkE;IAClE,uEAAuE;IACvE,mEAAmE;IACnE,mEAAmE;IACnE,iCAAiC;IACjC,MAAM,kBAAkB,GAA2B,EAAE,CAAC;IACtD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;QAClE,kBAAkB,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI;QAAE,kBAAkB,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACnE,IAAI,MAAM,CAAC,kBAAkB;QAAE,kBAAkB,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,kBAAkB,CAAC;IAEnG,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAEvF,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC;QACvF,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { MultibaseDigest } from '../multibase-digest/index.js';
|
|
3
|
+
import { ResolverNetworkError } from './errors.js';
|
|
4
|
+
const resolveDocument = jest.fn();
|
|
5
|
+
jest.unstable_mockModule('./resolve-document.js', () => ({
|
|
6
|
+
resolveDocument,
|
|
7
|
+
}));
|
|
8
|
+
const { resolveDocumentIfChanged } = await import('./resolve-document-if-changed.js');
|
|
9
|
+
async function digestOf(text) {
|
|
10
|
+
return MultibaseDigest.fromData(new TextEncoder().encode(text), { algorithm: 'sha2-256', base: 'base58btc' });
|
|
11
|
+
}
|
|
12
|
+
function loadResult(overrides) {
|
|
13
|
+
return {
|
|
14
|
+
status: 200,
|
|
15
|
+
body: new Uint8Array(0),
|
|
16
|
+
finalUrl: '',
|
|
17
|
+
bodyDigest: overrides.bodyDigest,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe('resolveDocumentIfChanged', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resolveDocument.mockReset();
|
|
24
|
+
});
|
|
25
|
+
describe('conditional headers', () => {
|
|
26
|
+
it('sends If-None-Match when cached.etag is supplied', async () => {
|
|
27
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 304, bodyDigest: await digestOf('') }));
|
|
28
|
+
await resolveDocumentIfChanged('https://example.com/', { etag: '"abc"' });
|
|
29
|
+
const passedOptions = resolveDocument.mock.calls[0][1];
|
|
30
|
+
expect(passedOptions.headers['if-none-match']).toBe('"abc"');
|
|
31
|
+
});
|
|
32
|
+
it('sends If-Modified-Since when cached.lastModifiedHeader is supplied', async () => {
|
|
33
|
+
const ts = 'Wed, 21 May 2026 12:00:00 GMT';
|
|
34
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 304, bodyDigest: await digestOf('') }));
|
|
35
|
+
await resolveDocumentIfChanged('https://example.com/', { lastModifiedHeader: ts });
|
|
36
|
+
const passedOptions = resolveDocument.mock.calls[0][1];
|
|
37
|
+
expect(passedOptions.headers['if-modified-since']).toBe(ts);
|
|
38
|
+
});
|
|
39
|
+
it('preserves caller-supplied headers alongside conditional ones', async () => {
|
|
40
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 304, bodyDigest: await digestOf('') }));
|
|
41
|
+
await resolveDocumentIfChanged('https://example.com/', { etag: '"abc"' }, { headers: { Accept: 'application/json' } });
|
|
42
|
+
const passedOptions = resolveDocument.mock.calls[0][1];
|
|
43
|
+
expect(passedOptions.headers['accept']).toBe('application/json');
|
|
44
|
+
expect(passedOptions.headers['if-none-match']).toBe('"abc"');
|
|
45
|
+
});
|
|
46
|
+
it('does not emit duplicate conditional headers when the caller supplies a differently-cased variant', async () => {
|
|
47
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 304, bodyDigest: await digestOf('') }));
|
|
48
|
+
await resolveDocumentIfChanged('https://example.com/', { etag: '"new"' },
|
|
49
|
+
// Caller passes a stale variant under a differently-cased key.
|
|
50
|
+
// The cached etag must win and only one normalised header lands on the wire.
|
|
51
|
+
{ headers: { 'IF-NONE-MATCH': '"stale"' } });
|
|
52
|
+
const passedOptions = resolveDocument.mock.calls[0][1];
|
|
53
|
+
const keys = Object.keys(passedOptions.headers);
|
|
54
|
+
const ifNoneMatchKeys = keys.filter((k) => k.toLowerCase() === 'if-none-match');
|
|
55
|
+
expect(ifNoneMatchKeys).toEqual(['if-none-match']);
|
|
56
|
+
expect(passedOptions.headers['if-none-match']).toBe('"new"');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('skip chain', () => {
|
|
60
|
+
it('returns { kind: "unchanged" } on a 304 response', async () => {
|
|
61
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 304, bodyDigest: await digestOf('') }));
|
|
62
|
+
await expect(resolveDocumentIfChanged('https://example.com/', { etag: '"abc"' })).resolves.toEqual({
|
|
63
|
+
kind: 'unchanged',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('returns { kind: "unchanged" } on a 200 response whose body digest matches the cached one', async () => {
|
|
67
|
+
const digest = await digestOf('same content');
|
|
68
|
+
resolveDocument.mockResolvedValue(loadResult({ status: 200, body: new TextEncoder().encode('same content'), bodyDigest: digest }));
|
|
69
|
+
await expect(resolveDocumentIfChanged('https://example.com/', { bodyDigest: digest })).resolves.toEqual({
|
|
70
|
+
kind: 'unchanged',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('returns { kind: "loaded", result } on a 200 response with a different body digest', async () => {
|
|
74
|
+
const oldDigest = await digestOf('old content');
|
|
75
|
+
const newDigest = await digestOf('new content');
|
|
76
|
+
const result = loadResult({
|
|
77
|
+
status: 200,
|
|
78
|
+
body: new TextEncoder().encode('new content'),
|
|
79
|
+
finalUrl: 'https://example.com/',
|
|
80
|
+
bodyDigest: newDigest,
|
|
81
|
+
});
|
|
82
|
+
resolveDocument.mockResolvedValue(result);
|
|
83
|
+
await expect(resolveDocumentIfChanged('https://example.com/', { bodyDigest: oldDigest })).resolves.toEqual({
|
|
84
|
+
kind: 'loaded',
|
|
85
|
+
result,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it('returns { kind: "loaded", result } on a 200 response when no cached body digest is supplied', async () => {
|
|
89
|
+
const result = loadResult({
|
|
90
|
+
status: 200,
|
|
91
|
+
body: new TextEncoder().encode('fresh'),
|
|
92
|
+
finalUrl: 'https://example.com/',
|
|
93
|
+
bodyDigest: await digestOf('fresh'),
|
|
94
|
+
});
|
|
95
|
+
resolveDocument.mockResolvedValue(result);
|
|
96
|
+
await expect(resolveDocumentIfChanged('https://example.com/', { etag: '"abc"' })).resolves.toEqual({
|
|
97
|
+
kind: 'loaded',
|
|
98
|
+
result,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('error propagation', () => {
|
|
103
|
+
it('propagates errors thrown by the underlying resolveDocument call', async () => {
|
|
104
|
+
resolveDocument.mockRejectedValue(new ResolverNetworkError('https://example.com/', new Error('boom')));
|
|
105
|
+
await expect(resolveDocumentIfChanged('https://example.com/', {})).rejects.toBeInstanceOf(ResolverNetworkError);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=resolve-document-if-changed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-document-if-changed.test.js","sourceRoot":"","sources":["../../src/resolvers/resolve-document-if-changed.test.ts"],"names":[],"mappings":"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"}
|