@open-operational-state/discovery 0.1.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/README.md +68 -0
- package/dist/__tests__/discovery.test.d.ts +8 -0
- package/dist/__tests__/discovery.test.d.ts.map +1 -0
- package/dist/__tests__/discovery.test.js +119 -0
- package/dist/__tests__/discovery.test.js.map +1 -0
- package/dist/discover.d.ts +34 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +62 -0
- package/dist/discover.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/link-header.d.ts +18 -0
- package/dist/link-header.d.ts.map +1 -0
- package/dist/link-header.js +98 -0
- package/dist/link-header.js.map +1 -0
- package/dist/well-known.d.ts +17 -0
- package/dist/well-known.d.ts.map +1 -0
- package/dist/well-known.js +129 -0
- package/dist/well-known.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @open-operational-state/discovery
|
|
2
|
+
|
|
3
|
+
Discovery client for [Open Operational State](https://github.com/open-operational-state). Find and validate operational-state endpoints using HTTP Link headers and well-known paths.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @open-operational-state/discovery
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### `discover( baseUrl, options? )`
|
|
14
|
+
|
|
15
|
+
Discover operational-state resources following the priority hierarchy:
|
|
16
|
+
|
|
17
|
+
1. Link-based discovery (HTTP `Link` headers with `rel="operational-state"`)
|
|
18
|
+
2. Well-known path (`/.well-known/operational-state`)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { discover } from '@open-operational-state/discovery';
|
|
22
|
+
|
|
23
|
+
const result = await discover( 'https://api.example.com' );
|
|
24
|
+
|
|
25
|
+
switch ( result.method ) {
|
|
26
|
+
case 'link-header':
|
|
27
|
+
console.log( 'Found via Link header:', result.links );
|
|
28
|
+
break;
|
|
29
|
+
case 'well-known':
|
|
30
|
+
console.log( 'Found via well-known:', result.document );
|
|
31
|
+
break;
|
|
32
|
+
case 'none':
|
|
33
|
+
console.log( 'No operational-state resources found' );
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `parseLinkHeaders( headers )`
|
|
39
|
+
|
|
40
|
+
Parse HTTP `Link` headers and extract `rel="operational-state"` entries.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { parseLinkHeaders } from '@open-operational-state/discovery';
|
|
44
|
+
|
|
45
|
+
const links = parseLinkHeaders(
|
|
46
|
+
'<https://api.example.com/health>; rel="operational-state"; profile="health"'
|
|
47
|
+
);
|
|
48
|
+
// [{ href: 'https://api.example.com/health', profile: 'health' }]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `fetchDiscoveryDocument( baseUrl )`
|
|
52
|
+
|
|
53
|
+
Fetch and validate the discovery document from `/.well-known/operational-state`.
|
|
54
|
+
|
|
55
|
+
### `validateDiscoveryDocument( doc )`
|
|
56
|
+
|
|
57
|
+
Validate the structure of a discovery document.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { validateDiscoveryDocument } from '@open-operational-state/discovery';
|
|
61
|
+
|
|
62
|
+
const result = validateDiscoveryDocument( doc );
|
|
63
|
+
// { valid: true, errors: [], warnings: [] }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Dependencies
|
|
67
|
+
|
|
68
|
+
- `@open-operational-state/types`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/discovery.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests Link header parsing and discovery document validation.
|
|
5
|
+
* (Network-dependent tests like fetch are skipped in unit testing.)
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'bun:test';
|
|
8
|
+
import { parseLinkHeaders } from '../link-header.js';
|
|
9
|
+
import { validateDiscoveryDocument } from '../well-known.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Link header parsing
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
describe('parseLinkHeaders', () => {
|
|
14
|
+
it('parses a simple operational-state link', () => {
|
|
15
|
+
const links = parseLinkHeaders('<https://api.example.com/health>; rel="operational-state"');
|
|
16
|
+
expect(links.length).toBe(1);
|
|
17
|
+
expect(links[0].href).toBe('https://api.example.com/health');
|
|
18
|
+
expect(links[0].profile).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
it('parses a link with profile parameter', () => {
|
|
21
|
+
const links = parseLinkHeaders('<https://api.example.com/health>; rel="operational-state"; profile="health"');
|
|
22
|
+
expect(links.length).toBe(1);
|
|
23
|
+
expect(links[0].href).toBe('https://api.example.com/health');
|
|
24
|
+
expect(links[0].profile).toBe('health');
|
|
25
|
+
});
|
|
26
|
+
it('parses multiple links', () => {
|
|
27
|
+
const links = parseLinkHeaders('<https://api.example.com/health>; rel="operational-state"; profile="health", ' +
|
|
28
|
+
'<https://api.example.com/status>; rel="operational-state"; profile="status"');
|
|
29
|
+
expect(links.length).toBe(2);
|
|
30
|
+
expect(links[0].profile).toBe('health');
|
|
31
|
+
expect(links[1].profile).toBe('status');
|
|
32
|
+
});
|
|
33
|
+
it('filters non-operational-state links', () => {
|
|
34
|
+
const links = parseLinkHeaders('<https://example.com>; rel="self", ' +
|
|
35
|
+
'<https://api.example.com/health>; rel="operational-state"');
|
|
36
|
+
expect(links.length).toBe(1);
|
|
37
|
+
expect(links[0].href).toBe('https://api.example.com/health');
|
|
38
|
+
});
|
|
39
|
+
it('handles header object', () => {
|
|
40
|
+
const links = parseLinkHeaders({
|
|
41
|
+
'Link': '<https://api.example.com/health>; rel="operational-state"',
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
});
|
|
44
|
+
expect(links.length).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
it('returns empty for no link header', () => {
|
|
47
|
+
const links = parseLinkHeaders({ 'Content-Type': 'application/json' });
|
|
48
|
+
expect(links.length).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Discovery document validation
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
describe('validateDiscoveryDocument', () => {
|
|
55
|
+
it('validates a correct document', () => {
|
|
56
|
+
const doc = {
|
|
57
|
+
version: '1.0',
|
|
58
|
+
subject: {
|
|
59
|
+
id: 'payment-platform',
|
|
60
|
+
description: 'Payment Processing Platform',
|
|
61
|
+
},
|
|
62
|
+
resources: [
|
|
63
|
+
{
|
|
64
|
+
href: 'https://api.example.com/health',
|
|
65
|
+
profiles: ['liveness', 'readiness', 'health'],
|
|
66
|
+
serialization: 'application/health+json',
|
|
67
|
+
auth: 'none',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const result = validateDiscoveryDocument(doc);
|
|
72
|
+
expect(result.valid).toBe(true);
|
|
73
|
+
expect(result.errors.length).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
it('rejects missing version', () => {
|
|
76
|
+
const doc = {
|
|
77
|
+
subject: { id: 'test' },
|
|
78
|
+
resources: [],
|
|
79
|
+
};
|
|
80
|
+
const result = validateDiscoveryDocument(doc);
|
|
81
|
+
expect(result.valid).toBe(false);
|
|
82
|
+
expect(result.errors.some((e) => e.code === 'DISCOVERY_INVALID_VERSION')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it('rejects missing subject', () => {
|
|
85
|
+
const doc = {
|
|
86
|
+
version: '1.0',
|
|
87
|
+
resources: [],
|
|
88
|
+
};
|
|
89
|
+
const result = validateDiscoveryDocument(doc);
|
|
90
|
+
expect(result.valid).toBe(false);
|
|
91
|
+
expect(result.errors.some((e) => e.code === 'DISCOVERY_MISSING_SUBJECT')).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
it('rejects missing resources', () => {
|
|
94
|
+
const doc = {
|
|
95
|
+
version: '1.0',
|
|
96
|
+
subject: { id: 'test' },
|
|
97
|
+
};
|
|
98
|
+
const result = validateDiscoveryDocument(doc);
|
|
99
|
+
expect(result.valid).toBe(false);
|
|
100
|
+
expect(result.errors.some((e) => e.code === 'DISCOVERY_MISSING_RESOURCES')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it('validates resource entries', () => {
|
|
103
|
+
const doc = {
|
|
104
|
+
version: '1.0',
|
|
105
|
+
subject: { id: 'test' },
|
|
106
|
+
resources: [
|
|
107
|
+
{ profiles: ['health'], serialization: 'application/health+json' },
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
const result = validateDiscoveryDocument(doc);
|
|
111
|
+
expect(result.valid).toBe(false);
|
|
112
|
+
expect(result.errors.some((e) => e.code === 'DISCOVERY_MISSING_HREF')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('rejects non-object input', () => {
|
|
115
|
+
const result = validateDiscoveryDocument(null);
|
|
116
|
+
expect(result.valid).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=discovery.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.test.js","sourceRoot":"","sources":["../../src/__tests__/discovery.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Discovery Flow
|
|
3
|
+
*
|
|
4
|
+
* Implements the discovery priority hierarchy:
|
|
5
|
+
* 1. Link-based discovery (primary)
|
|
6
|
+
* 2. Well-known path (baseline fallback)
|
|
7
|
+
*/
|
|
8
|
+
import type { DiscoveryDocument } from '@open-operational-state/types';
|
|
9
|
+
import type { OperationalStateLink } from './link-header.js';
|
|
10
|
+
export interface DiscoverOptions {
|
|
11
|
+
/** Skip Link header discovery */
|
|
12
|
+
skipLinkHeaders?: boolean;
|
|
13
|
+
/** Skip well-known path discovery */
|
|
14
|
+
skipWellKnown?: boolean;
|
|
15
|
+
/** Custom headers to send with requests */
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface DiscoverResult {
|
|
19
|
+
/** How the resources were discovered */
|
|
20
|
+
method: 'link-header' | 'well-known' | 'none';
|
|
21
|
+
/** Operational state links found via Link headers */
|
|
22
|
+
links: OperationalStateLink[];
|
|
23
|
+
/** Discovery document from well-known path (if found) */
|
|
24
|
+
document: DiscoveryDocument | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Discover operational-state resources for a given base URL.
|
|
28
|
+
*
|
|
29
|
+
* Follows the discovery priority hierarchy:
|
|
30
|
+
* 1. Check Link headers from a probe of the base URL
|
|
31
|
+
* 2. Fall back to /.well-known/operational-state
|
|
32
|
+
*/
|
|
33
|
+
export declare function discover(baseUrl: string, options?: DiscoverOptions): Promise<DiscoverResult>;
|
|
34
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAO7D,MAAM,WAAW,eAAe;IAC5B,iCAAiC;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC3B,wCAAwC;IACxC,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;IAC9C,qDAAqD;IACrD,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,yDAAyD;IACzD,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC;AAMD;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC1B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GAC1B,OAAO,CAAC,cAAc,CAAC,CA2CzB"}
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Discovery Flow
|
|
3
|
+
*
|
|
4
|
+
* Implements the discovery priority hierarchy:
|
|
5
|
+
* 1. Link-based discovery (primary)
|
|
6
|
+
* 2. Well-known path (baseline fallback)
|
|
7
|
+
*/
|
|
8
|
+
import { parseLinkHeaders } from './link-header.js';
|
|
9
|
+
import { fetchDiscoveryDocument } from './well-known.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Public API
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Discover operational-state resources for a given base URL.
|
|
15
|
+
*
|
|
16
|
+
* Follows the discovery priority hierarchy:
|
|
17
|
+
* 1. Check Link headers from a probe of the base URL
|
|
18
|
+
* 2. Fall back to /.well-known/operational-state
|
|
19
|
+
*/
|
|
20
|
+
export async function discover(baseUrl, options) {
|
|
21
|
+
// ── 1. Link header discovery ───────────────────────────────────────
|
|
22
|
+
if (!options?.skipLinkHeaders) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(baseUrl, {
|
|
25
|
+
method: 'HEAD',
|
|
26
|
+
headers: options?.headers,
|
|
27
|
+
});
|
|
28
|
+
const linkHeader = response.headers.get('link');
|
|
29
|
+
if (linkHeader) {
|
|
30
|
+
const links = parseLinkHeaders(linkHeader);
|
|
31
|
+
if (links.length > 0) {
|
|
32
|
+
return {
|
|
33
|
+
method: 'link-header',
|
|
34
|
+
links,
|
|
35
|
+
document: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Link header discovery failed, fall through to well-known
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── 2. Well-known path fallback ────────────────────────────────────
|
|
45
|
+
if (!options?.skipWellKnown) {
|
|
46
|
+
const doc = await fetchDiscoveryDocument(baseUrl);
|
|
47
|
+
if (doc) {
|
|
48
|
+
return {
|
|
49
|
+
method: 'well-known',
|
|
50
|
+
links: [],
|
|
51
|
+
document: doc,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ── No discovery available ─────────────────────────────────────────
|
|
56
|
+
return {
|
|
57
|
+
method: 'none',
|
|
58
|
+
links: [],
|
|
59
|
+
document: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAwBzD,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC1B,OAAe,EACf,OAAyB;IAEzB,sEAAsE;IACtE,IAAK,CAAC,OAAO,EAAE,eAAe,EAAG,CAAC;QAC9B,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAE,OAAO,EAAE;gBACnC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,OAAO,EAAE,OAAO;aAC5B,CAAE,CAAC;YAEJ,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAE,MAAM,CAAE,CAAC;YAClD,IAAK,UAAU,EAAG,CAAC;gBACf,MAAM,KAAK,GAAG,gBAAgB,CAAE,UAAU,CAAE,CAAC;gBAC7C,IAAK,KAAK,CAAC,MAAM,GAAG,CAAC,EAAG,CAAC;oBACrB,OAAO;wBACH,MAAM,EAAE,aAAa;wBACrB,KAAK;wBACL,QAAQ,EAAE,IAAI;qBACjB,CAAC;gBACN,CAAC;YACL,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,2DAA2D;QAC/D,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,IAAK,CAAC,OAAO,EAAE,aAAa,EAAG,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,sBAAsB,CAAE,OAAO,CAAE,CAAC;QACpD,IAAK,GAAG,EAAG,CAAC;YACR,OAAO;gBACH,MAAM,EAAE,YAAY;gBACpB,KAAK,EAAE,EAAE;gBACT,QAAQ,EAAE,GAAG;aAChB,CAAC;QACN,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,OAAO;QACH,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,EAAE;QACT,QAAQ,EAAE,IAAI;KACjB,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @open-operational-state/discovery
|
|
3
|
+
*
|
|
4
|
+
* Discovery client — well-known paths, Link header parsing,
|
|
5
|
+
* discovery document consumption.
|
|
6
|
+
*
|
|
7
|
+
* Depends on @open-operational-state/types.
|
|
8
|
+
*/
|
|
9
|
+
export { parseLinkHeaders } from './link-header.js';
|
|
10
|
+
export type { OperationalStateLink } from './link-header.js';
|
|
11
|
+
export { fetchDiscoveryDocument, validateDiscoveryDocument } from './well-known.js';
|
|
12
|
+
export { discover } from './discover.js';
|
|
13
|
+
export type { DiscoverOptions, DiscoverResult } from './discover.js';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @open-operational-state/discovery
|
|
3
|
+
*
|
|
4
|
+
* Discovery client — well-known paths, Link header parsing,
|
|
5
|
+
* discovery document consumption.
|
|
6
|
+
*
|
|
7
|
+
* Depends on @open-operational-state/types.
|
|
8
|
+
*/
|
|
9
|
+
export { parseLinkHeaders } from './link-header.js';
|
|
10
|
+
export { fetchDiscoveryDocument, validateDiscoveryDocument } from './well-known.js';
|
|
11
|
+
export { discover } from './discover.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Header Parsing
|
|
3
|
+
*
|
|
4
|
+
* Parses HTTP Link headers per RFC 8288, extracting
|
|
5
|
+
* rel="operational-state" entries with optional profile parameter.
|
|
6
|
+
*/
|
|
7
|
+
export interface OperationalStateLink {
|
|
8
|
+
href: string;
|
|
9
|
+
profile?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse HTTP Link headers and extract operational-state links.
|
|
13
|
+
*
|
|
14
|
+
* Input may be a single header string or an object with header key/values.
|
|
15
|
+
* Multiple Link headers are joined by comma (per HTTP spec).
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseLinkHeaders(headers: Record<string, string> | string): OperationalStateLink[];
|
|
18
|
+
//# sourceMappingURL=link-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-header.d.ts","sourceRoot":"","sources":["../src/link-header.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GACzC,oBAAoB,EAAE,CAiCxB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Header Parsing
|
|
3
|
+
*
|
|
4
|
+
* Parses HTTP Link headers per RFC 8288, extracting
|
|
5
|
+
* rel="operational-state" entries with optional profile parameter.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Public API
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Parse HTTP Link headers and extract operational-state links.
|
|
12
|
+
*
|
|
13
|
+
* Input may be a single header string or an object with header key/values.
|
|
14
|
+
* Multiple Link headers are joined by comma (per HTTP spec).
|
|
15
|
+
*/
|
|
16
|
+
export function parseLinkHeaders(headers) {
|
|
17
|
+
let raw;
|
|
18
|
+
if (typeof headers === 'string') {
|
|
19
|
+
raw = headers;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Find the Link header (case-insensitive)
|
|
23
|
+
raw = '';
|
|
24
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
25
|
+
if (key.toLowerCase() === 'link') {
|
|
26
|
+
raw = value;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!raw) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const links = [];
|
|
35
|
+
// Split by comma (respecting angle brackets)
|
|
36
|
+
const entries = splitLinkEntries(raw);
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const parsed = parseLinkEntry(entry);
|
|
39
|
+
if (parsed && parsed.rel === 'operational-state') {
|
|
40
|
+
const link = { href: parsed.href };
|
|
41
|
+
if (parsed.profile) {
|
|
42
|
+
link.profile = parsed.profile;
|
|
43
|
+
}
|
|
44
|
+
links.push(link);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return links;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Split a Link header into individual entries, respecting angle brackets.
|
|
51
|
+
*/
|
|
52
|
+
function splitLinkEntries(raw) {
|
|
53
|
+
const entries = [];
|
|
54
|
+
let depth = 0;
|
|
55
|
+
let start = 0;
|
|
56
|
+
for (let i = 0; i < raw.length; i++) {
|
|
57
|
+
if (raw[i] === '<') {
|
|
58
|
+
depth++;
|
|
59
|
+
}
|
|
60
|
+
else if (raw[i] === '>') {
|
|
61
|
+
depth--;
|
|
62
|
+
}
|
|
63
|
+
else if (raw[i] === ',' && depth === 0) {
|
|
64
|
+
entries.push(raw.slice(start, i).trim());
|
|
65
|
+
start = i + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const last = raw.slice(start).trim();
|
|
69
|
+
if (last) {
|
|
70
|
+
entries.push(last);
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse a single Link entry: `<url>; rel="..."; profile="..."`
|
|
76
|
+
*/
|
|
77
|
+
function parseLinkEntry(entry) {
|
|
78
|
+
// Extract URL from angle brackets
|
|
79
|
+
const hrefMatch = entry.match(/^<([^>]+)>(.*)$/);
|
|
80
|
+
if (!hrefMatch) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const href = hrefMatch[1];
|
|
84
|
+
const params = hrefMatch[2];
|
|
85
|
+
// Extract parameters
|
|
86
|
+
let rel = '';
|
|
87
|
+
let profile;
|
|
88
|
+
const relMatch = params.match(/;\s*rel\s*=\s*"([^"]+)"/);
|
|
89
|
+
if (relMatch) {
|
|
90
|
+
rel = relMatch[1];
|
|
91
|
+
}
|
|
92
|
+
const profileMatch = params.match(/;\s*profile\s*=\s*"([^"]+)"/);
|
|
93
|
+
if (profileMatch) {
|
|
94
|
+
profile = profileMatch[1];
|
|
95
|
+
}
|
|
96
|
+
return { href, rel, profile };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=link-header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-header.js","sourceRoot":"","sources":["../src/link-header.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-Known Path Resolution
|
|
3
|
+
*
|
|
4
|
+
* Fetch and validate discovery documents from /.well-known/operational-state
|
|
5
|
+
*/
|
|
6
|
+
import type { DiscoveryDocument, ValidationResult } from '@open-operational-state/types';
|
|
7
|
+
/**
|
|
8
|
+
* Fetch the discovery document from /.well-known/operational-state.
|
|
9
|
+
*
|
|
10
|
+
* Returns null if the endpoint is not available or returns invalid data.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchDiscoveryDocument(baseUrl: string): Promise<DiscoveryDocument | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Validate the structure of a discovery document.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateDiscoveryDocument(doc: unknown): ValidationResult;
|
|
17
|
+
//# sourceMappingURL=well-known.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"well-known.d.ts","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACR,iBAAiB,EACjB,gBAAgB,EAGnB,MAAM,+BAA+B,CAAC;AAQvC;;;;GAIG;AACH,wBAAsB,sBAAsB,CACxC,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAgBnC;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAE,GAAG,EAAE,OAAO,GAAI,gBAAgB,CAmG1E"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-Known Path Resolution
|
|
3
|
+
*
|
|
4
|
+
* Fetch and validate discovery documents from /.well-known/operational-state
|
|
5
|
+
*/
|
|
6
|
+
import { isProfileId } from '@open-operational-state/types';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Public API
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the discovery document from /.well-known/operational-state.
|
|
12
|
+
*
|
|
13
|
+
* Returns null if the endpoint is not available or returns invalid data.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchDiscoveryDocument(baseUrl) {
|
|
16
|
+
const url = new URL('/.well-known/operational-state', baseUrl).href;
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const body = await response.json();
|
|
23
|
+
const validation = validateDiscoveryDocument(body);
|
|
24
|
+
if (!validation.valid) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate the structure of a discovery document.
|
|
35
|
+
*/
|
|
36
|
+
export function validateDiscoveryDocument(doc) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
const warnings = [];
|
|
39
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
40
|
+
errors.push({
|
|
41
|
+
path: '',
|
|
42
|
+
message: 'Discovery document must be a non-null object',
|
|
43
|
+
code: 'DISCOVERY_INVALID_TYPE',
|
|
44
|
+
});
|
|
45
|
+
return { valid: false, errors, warnings };
|
|
46
|
+
}
|
|
47
|
+
const obj = doc;
|
|
48
|
+
// version
|
|
49
|
+
if (obj.version !== '1.0') {
|
|
50
|
+
errors.push({
|
|
51
|
+
path: 'version',
|
|
52
|
+
message: `Discovery document version must be '1.0', got '${obj.version}'`,
|
|
53
|
+
code: 'DISCOVERY_INVALID_VERSION',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// subject
|
|
57
|
+
if (!obj.subject || typeof obj.subject !== 'object' || Array.isArray(obj.subject)) {
|
|
58
|
+
errors.push({
|
|
59
|
+
path: 'subject',
|
|
60
|
+
message: 'Discovery document must have a subject object',
|
|
61
|
+
code: 'DISCOVERY_MISSING_SUBJECT',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const subject = obj.subject;
|
|
66
|
+
if (typeof subject.id !== 'string' || !subject.id) {
|
|
67
|
+
errors.push({
|
|
68
|
+
path: 'subject.id',
|
|
69
|
+
message: 'Discovery document subject must have an id',
|
|
70
|
+
code: 'DISCOVERY_MISSING_SUBJECT_ID',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// resources
|
|
75
|
+
if (!Array.isArray(obj.resources)) {
|
|
76
|
+
errors.push({
|
|
77
|
+
path: 'resources',
|
|
78
|
+
message: 'Discovery document must have a resources array',
|
|
79
|
+
code: 'DISCOVERY_MISSING_RESOURCES',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
for (let i = 0; i < obj.resources.length; i++) {
|
|
84
|
+
const resource = obj.resources[i];
|
|
85
|
+
if (typeof resource.href !== 'string' || !resource.href) {
|
|
86
|
+
errors.push({
|
|
87
|
+
path: `resources[${i}].href`,
|
|
88
|
+
message: `Resource ${i} must have an href`,
|
|
89
|
+
code: 'DISCOVERY_MISSING_HREF',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (!Array.isArray(resource.profiles) || resource.profiles.length === 0) {
|
|
93
|
+
errors.push({
|
|
94
|
+
path: `resources[${i}].profiles`,
|
|
95
|
+
message: `Resource ${i} must have a non-empty profiles array`,
|
|
96
|
+
code: 'DISCOVERY_MISSING_PROFILES',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
for (const p of resource.profiles) {
|
|
101
|
+
if (typeof p === 'string' && !isProfileId(p)) {
|
|
102
|
+
warnings.push({
|
|
103
|
+
path: `resources[${i}].profiles`,
|
|
104
|
+
message: `Unknown profile identifier: '${p}'`,
|
|
105
|
+
code: 'DISCOVERY_UNKNOWN_PROFILE',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (typeof resource.serialization !== 'string' || !resource.serialization) {
|
|
111
|
+
errors.push({
|
|
112
|
+
path: `resources[${i}].serialization`,
|
|
113
|
+
message: `Resource ${i} must have a serialization`,
|
|
114
|
+
code: 'DISCOVERY_MISSING_SERIALIZATION',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// auth is RECOMMENDED, not REQUIRED — only warn
|
|
118
|
+
if (resource.auth !== undefined && resource.auth !== 'none' && resource.auth !== 'required') {
|
|
119
|
+
warnings.push({
|
|
120
|
+
path: `resources[${i}].auth`,
|
|
121
|
+
message: `Resource ${i} auth should be 'none' or 'required', got '${resource.auth}'`,
|
|
122
|
+
code: 'DISCOVERY_INVALID_AUTH',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=well-known.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"well-known.js","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-operational-state/discovery",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Discovery client for locating Open Operational State resources",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/open-operational-state/status-tooling.git",
|
|
24
|
+
"directory": "packages/discovery"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "bun test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@open-operational-state/types": "^0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.8.0"
|
|
37
|
+
}
|
|
38
|
+
}
|