@astrale-os/kernel-client 0.6.0-beta.82 → 0.6.0-beta.84
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/dist/client/admission.js +1 -9
- package/dist/client/client.js +3 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/routing-control.d.ts +11 -3
- package/dist/client/routing-control.js +18 -3
- package/dist/errors/stale-route-error.d.ts +14 -3
- package/dist/errors/stale-route-error.js +15 -3
- package/dist/errors/transport-error.d.ts +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/introspection.js +10 -6
- package/dist/schema/schema.d.ts +3 -0
- package/dist/schema/schema.js +13 -2
- package/dist/session/bound.js +10 -1
- package/dist/session/content.js +4 -27
- package/dist/session/context/index.d.ts +1 -1
- package/dist/session/context/index.js +1 -1
- package/dist/session/context/limits.d.ts +1 -0
- package/dist/session/context/limits.js +1 -0
- package/dist/session/discovery/bundle.js +1 -1
- package/dist/session/discovery/discovery.d.ts +12 -4
- package/dist/session/discovery/discovery.js +28 -19
- package/dist/session/discovery/index.d.ts +8 -1
- package/dist/session/discovery/index.js +5 -8
- package/dist/session/discovery/legacy/index.d.ts +38 -0
- package/dist/session/discovery/legacy/index.js +38 -0
- package/dist/session/discovery/legacy/intrinsic.d.ts +33 -0
- package/dist/session/discovery/legacy/intrinsic.js +67 -0
- package/dist/session/discovery/legacy/projection.d.ts +8 -0
- package/dist/session/discovery/legacy/projection.js +17 -0
- package/dist/session/discovery/{publication.d.ts → legacy/publication.d.ts} +8 -5
- package/dist/session/discovery/{publication.js → legacy/publication.js} +13 -60
- package/dist/session/discovery/release.d.ts +29 -0
- package/dist/session/discovery/release.js +81 -0
- package/dist/session/discovery/response.d.ts +3 -0
- package/dist/session/discovery/response.js +59 -0
- package/dist/session/discovery/snapshot.d.ts +9 -2
- package/dist/session/discovery/snapshot.js +2 -2
- package/dist/session/discovery/transport.d.ts +1 -3
- package/dist/session/exchange/options.d.ts +13 -10
- package/dist/session/exchange/options.js +6 -5
- package/dist/session/exchange/presentation.d.ts +3 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/reference.d.ts +3 -6
- package/dist/session/reference.js +0 -4
- package/dist/session/routing/dispatch.js +36 -37
- package/dist/session/session.d.ts +18 -3
- package/dist/session/session.js +21 -10
- package/dist/session/session.typecheck.js +14 -3
- package/dist/transport/auto/transport.d.ts +0 -1
- package/dist/transport/auto/transport.js +0 -2
- package/dist/transport/http/response.d.ts +1 -1
- package/dist/transport/http/response.js +2 -6
- package/dist/transport/http/transport.d.ts +5 -3
- package/dist/transport/http/transport.js +40 -19
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/transport.d.ts +10 -2
- package/dist/transport/websocket/transport.d.ts +0 -3
- package/dist/transport/websocket/transport.js +1 -16
- package/package.json +6 -7
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility discovery for Hosts without `release.json` and callers of the Publication API.
|
|
3
|
+
* Release-backed Hosts expose a locally computed v3 projection to those callers.
|
|
4
|
+
*
|
|
5
|
+
* Superseded by `ClientSession.release()` and `SessionSnapshot.release` (`ReleaseDiscovery`,
|
|
6
|
+
* `IntrinsicRelease`), and, for the stale hint, by `StaleRouteError.current`.
|
|
7
|
+
*
|
|
8
|
+
* Short-term consumers:
|
|
9
|
+
* - Hosts whose Kernel predates intrinsic `release.json`: they answer it with 404 (a browser sees
|
|
10
|
+
* a `TypeError`, the 404 carrying no CORS headers) and serve only their version 3 Publication at
|
|
11
|
+
* `domain.json`. Every Host does until it runs a Kernel release that serves `release.json`; the
|
|
12
|
+
* 1Pact path keeps one, because the client in GUI and Admin reads the Kernel .117 Host;
|
|
13
|
+
* - callers of the Publication API: Shell's `KernelClient.publication()` (and shell-react's graph
|
|
14
|
+
* store through it) and Admin's remote instance gateway (`SessionSnapshot.publication`), until
|
|
15
|
+
* each reads `release()` / `release`;
|
|
16
|
+
* - the snapshot's bundle and root Views, whose provenance (`ResolvedView.etag`) keeps the v3 ETag
|
|
17
|
+
* until the breaking release (AM-1).
|
|
18
|
+
*
|
|
19
|
+
* Removal, in a separate breaking release: no supported Host predates intrinsic `release.json`
|
|
20
|
+
* (never while GUI and Admin talk to the Kernel .117 Host), no supported consumer reads the
|
|
21
|
+
* Publication API, and the breaking release of AM-1 emits release digests on every
|
|
22
|
+
* Kernel-to-client wire value. Then delete, in the same PR:
|
|
23
|
+
* - this folder, its tests (the pre-v4 cases of `session/__tests__/release.test.ts`) and its laws;
|
|
24
|
+
* `SessionDiscovery` then reads `ReleaseDiscovery` directly, reports its 404 as a Client failure
|
|
25
|
+
* and designates a snapshot by release digest, so canonical code only narrows `IntrinsicRelease`
|
|
26
|
+
* to the `DomainRelease`;
|
|
27
|
+
* - the `'publication'` acquisition resource of `TransportError`, and the `domain.json` bound of
|
|
28
|
+
* `ClientSessionOptions.maximumPublicationBytes`, which keeps bounding the Publications Schema
|
|
29
|
+
* results carry until those name releases;
|
|
30
|
+
* - `ClientSession.publication()`, `SessionDiscovery.publication` and `SessionSnapshot.publication`
|
|
31
|
+
* with their `.spec` entries; `BundleDiscovery` and `rootViewCatalog` then take the release,
|
|
32
|
+
* whose View provenance is `ResolvedView.release`;
|
|
33
|
+
* - `StaleRouteError.currentEtag`, from law CLIENT-LEGACY-WIRE-FIELDS and its test, with the
|
|
34
|
+
* Protocol fields of law PROTOCOL-LEGACY-WIRE-FIELDS.
|
|
35
|
+
*/
|
|
36
|
+
export { designation, IntrinsicReleaseDiscovery, type ReleaseReader } from './intrinsic.js';
|
|
37
|
+
export { PublicationDiscovery, type PublicationDiscoveryOptions } from './publication.js';
|
|
38
|
+
export { publicationOf } from './projection.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility discovery for Hosts without `release.json` and callers of the Publication API.
|
|
3
|
+
* Release-backed Hosts expose a locally computed v3 projection to those callers.
|
|
4
|
+
*
|
|
5
|
+
* Superseded by `ClientSession.release()` and `SessionSnapshot.release` (`ReleaseDiscovery`,
|
|
6
|
+
* `IntrinsicRelease`), and, for the stale hint, by `StaleRouteError.current`.
|
|
7
|
+
*
|
|
8
|
+
* Short-term consumers:
|
|
9
|
+
* - Hosts whose Kernel predates intrinsic `release.json`: they answer it with 404 (a browser sees
|
|
10
|
+
* a `TypeError`, the 404 carrying no CORS headers) and serve only their version 3 Publication at
|
|
11
|
+
* `domain.json`. Every Host does until it runs a Kernel release that serves `release.json`; the
|
|
12
|
+
* 1Pact path keeps one, because the client in GUI and Admin reads the Kernel .117 Host;
|
|
13
|
+
* - callers of the Publication API: Shell's `KernelClient.publication()` (and shell-react's graph
|
|
14
|
+
* store through it) and Admin's remote instance gateway (`SessionSnapshot.publication`), until
|
|
15
|
+
* each reads `release()` / `release`;
|
|
16
|
+
* - the snapshot's bundle and root Views, whose provenance (`ResolvedView.etag`) keeps the v3 ETag
|
|
17
|
+
* until the breaking release (AM-1).
|
|
18
|
+
*
|
|
19
|
+
* Removal, in a separate breaking release: no supported Host predates intrinsic `release.json`
|
|
20
|
+
* (never while GUI and Admin talk to the Kernel .117 Host), no supported consumer reads the
|
|
21
|
+
* Publication API, and the breaking release of AM-1 emits release digests on every
|
|
22
|
+
* Kernel-to-client wire value. Then delete, in the same PR:
|
|
23
|
+
* - this folder, its tests (the pre-v4 cases of `session/__tests__/release.test.ts`) and its laws;
|
|
24
|
+
* `SessionDiscovery` then reads `ReleaseDiscovery` directly, reports its 404 as a Client failure
|
|
25
|
+
* and designates a snapshot by release digest, so canonical code only narrows `IntrinsicRelease`
|
|
26
|
+
* to the `DomainRelease`;
|
|
27
|
+
* - the `'publication'` acquisition resource of `TransportError`, and the `domain.json` bound of
|
|
28
|
+
* `ClientSessionOptions.maximumPublicationBytes`, which keeps bounding the Publications Schema
|
|
29
|
+
* results carry until those name releases;
|
|
30
|
+
* - `ClientSession.publication()`, `SessionDiscovery.publication` and `SessionSnapshot.publication`
|
|
31
|
+
* with their `.spec` entries; `BundleDiscovery` and `rootViewCatalog` then take the release,
|
|
32
|
+
* whose View provenance is `ResolvedView.release`;
|
|
33
|
+
* - `StaleRouteError.currentEtag`, from law CLIENT-LEGACY-WIRE-FIELDS and its test, with the
|
|
34
|
+
* Protocol fields of law PROTOCOL-LEGACY-WIRE-FIELDS.
|
|
35
|
+
*/
|
|
36
|
+
export { designation, IntrinsicReleaseDiscovery } from './intrinsic.js';
|
|
37
|
+
export { PublicationDiscovery } from './publication.js';
|
|
38
|
+
export { publicationOf } from './projection.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { release } from '@astrale-os/kernel-protocol';
|
|
2
|
+
import type { SessionOperation } from '../../context/index.js';
|
|
3
|
+
import type { IntrinsicRelease } from '../release.js';
|
|
4
|
+
import type { PublicationDiscovery } from './publication.js';
|
|
5
|
+
/** Reprobe absent `release.json` after this interval so Host upgrades become visible. */
|
|
6
|
+
export declare const RELEASE_ABSENCE_MAXIMUM_AGE_MS = 60000;
|
|
7
|
+
export interface ReleaseReader {
|
|
8
|
+
current(): release.DomainRelease | undefined;
|
|
9
|
+
revalidate(operation: SessionOperation): Promise<release.DomainRelease | undefined>;
|
|
10
|
+
invalidate(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The intrinsic release of a Host whichever document it serves. `release.json` is read first; the
|
|
14
|
+
* version 3 Publication at `domain.json` is read in its place only when that URL answers its exact
|
|
15
|
+
* 404, or, while no `DomainRelease` has been admitted since construction, the last `invalidate()`
|
|
16
|
+
* or the last 404, when its Fetch fails before any response, which is how a browser reports a
|
|
17
|
+
* cross-origin 404 without CORS headers. Once a release is admitted, a failed Fetch is the release
|
|
18
|
+
* acquisition failure it is. An observed absence is trusted for `RELEASE_ABSENCE_MAXIMUM_AGE_MS`.
|
|
19
|
+
*
|
|
20
|
+
* @deprecated Successor: `ReleaseDiscovery` alone, once no supported Host predates intrinsic
|
|
21
|
+
* `release.json`. Deleted with this folder (see `index.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export declare class IntrinsicReleaseDiscovery {
|
|
24
|
+
private readonly releases;
|
|
25
|
+
private readonly publications;
|
|
26
|
+
private absentUntil;
|
|
27
|
+
private revision;
|
|
28
|
+
constructor(releases: ReleaseReader, publications: PublicationDiscovery);
|
|
29
|
+
revalidate(operation: SessionOperation): Promise<IntrinsicRelease>;
|
|
30
|
+
invalidate(): void;
|
|
31
|
+
}
|
|
32
|
+
/** The digest designating one intrinsic release: its release digest, or a pre-v4 ETag. */
|
|
33
|
+
export declare function designation(input: IntrinsicRelease): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { TransportError } from '../../../errors/index.js';
|
|
2
|
+
/** Reprobe absent `release.json` after this interval so Host upgrades become visible. */
|
|
3
|
+
export const RELEASE_ABSENCE_MAXIMUM_AGE_MS = 60_000;
|
|
4
|
+
/**
|
|
5
|
+
* The intrinsic release of a Host whichever document it serves. `release.json` is read first; the
|
|
6
|
+
* version 3 Publication at `domain.json` is read in its place only when that URL answers its exact
|
|
7
|
+
* 404, or, while no `DomainRelease` has been admitted since construction, the last `invalidate()`
|
|
8
|
+
* or the last 404, when its Fetch fails before any response, which is how a browser reports a
|
|
9
|
+
* cross-origin 404 without CORS headers. Once a release is admitted, a failed Fetch is the release
|
|
10
|
+
* acquisition failure it is. An observed absence is trusted for `RELEASE_ABSENCE_MAXIMUM_AGE_MS`.
|
|
11
|
+
*
|
|
12
|
+
* @deprecated Successor: `ReleaseDiscovery` alone, once no supported Host predates intrinsic
|
|
13
|
+
* `release.json`. Deleted with this folder (see `index.ts`).
|
|
14
|
+
*/
|
|
15
|
+
export class IntrinsicReleaseDiscovery {
|
|
16
|
+
releases;
|
|
17
|
+
publications;
|
|
18
|
+
absentUntil;
|
|
19
|
+
revision = 0;
|
|
20
|
+
constructor(releases, publications) {
|
|
21
|
+
this.releases = releases;
|
|
22
|
+
this.publications = publications;
|
|
23
|
+
}
|
|
24
|
+
async revalidate(operation) {
|
|
25
|
+
const revision = this.revision;
|
|
26
|
+
if (this.absentUntil === undefined || Date.now() >= this.absentUntil) {
|
|
27
|
+
const admitted = this.releases.current() !== undefined;
|
|
28
|
+
let served;
|
|
29
|
+
try {
|
|
30
|
+
served = await this.releases.revalidate(operation);
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
if (admitted || !isUnreadableAbsence(cause))
|
|
34
|
+
throw cause;
|
|
35
|
+
served = undefined;
|
|
36
|
+
}
|
|
37
|
+
if (revision !== this.revision)
|
|
38
|
+
return this.revalidate(operation);
|
|
39
|
+
this.absentUntil = undefined;
|
|
40
|
+
if (served !== undefined)
|
|
41
|
+
return served;
|
|
42
|
+
}
|
|
43
|
+
const legacy = await this.publications.revalidate(operation);
|
|
44
|
+
if (revision !== this.revision)
|
|
45
|
+
return this.revalidate(operation);
|
|
46
|
+
this.absentUntil ??= Date.now() + RELEASE_ABSENCE_MAXIMUM_AGE_MS;
|
|
47
|
+
return legacy;
|
|
48
|
+
}
|
|
49
|
+
invalidate() {
|
|
50
|
+
this.revision += 1;
|
|
51
|
+
this.absentUntil = undefined;
|
|
52
|
+
this.releases.invalidate();
|
|
53
|
+
this.publications.invalidate();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The digest designating one intrinsic release: its release digest, or a pre-v4 ETag. */
|
|
57
|
+
export function designation(input) {
|
|
58
|
+
return input.version === 4 ? input.digest : input.etag;
|
|
59
|
+
}
|
|
60
|
+
/** A `release.json` request that failed with the Fetch-standard network error, before any response. */
|
|
61
|
+
function isUnreadableAbsence(cause) {
|
|
62
|
+
return (cause instanceof TransportError &&
|
|
63
|
+
cause.phase === 'unknown' &&
|
|
64
|
+
cause.context.kind === 'acquisition' &&
|
|
65
|
+
cause.context.resource === 'release' &&
|
|
66
|
+
cause.cause instanceof TypeError);
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { publication } from '@astrale-os/kernel-protocol';
|
|
2
|
+
import type { IntrinsicRelease } from '../release.js';
|
|
3
|
+
/**
|
|
4
|
+
* Retain one Publication object per admitted release; bundle and View provenance use its ETag.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Use `IntrinsicRelease`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function publicationOf(input: IntrinsicRelease): publication.Publication;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as releaseLegacy from '@astrale-os/kernel-protocol/release/legacy';
|
|
2
|
+
const projections = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Retain one Publication object per admitted release; bundle and View provenance use its ETag.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Use `IntrinsicRelease`.
|
|
7
|
+
*/
|
|
8
|
+
export function publicationOf(input) {
|
|
9
|
+
if (input.version !== 4)
|
|
10
|
+
return input;
|
|
11
|
+
const retained = projections.get(input);
|
|
12
|
+
if (retained !== undefined)
|
|
13
|
+
return retained;
|
|
14
|
+
const projected = releaseLegacy.publication(input);
|
|
15
|
+
projections.set(input, projected);
|
|
16
|
+
return projected;
|
|
17
|
+
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
2
|
import type { publication } from '@astrale-os/kernel-protocol';
|
|
3
|
-
import type { Fetch } from '
|
|
4
|
-
import type { SessionOperation } from '
|
|
5
|
-
import { type SessionPolicy } from '
|
|
3
|
+
import type { Fetch } from '../../../transport/http/index.js';
|
|
4
|
+
import type { SessionOperation } from '../../context/index.js';
|
|
5
|
+
import { type SessionPolicy } from '../../context/index.js';
|
|
6
6
|
export interface PublicationDiscoveryOptions {
|
|
7
7
|
readonly kernel: IssuerId;
|
|
8
8
|
readonly fetch: Fetch;
|
|
9
9
|
readonly policy: Readonly<SessionPolicy>;
|
|
10
10
|
readonly maximumBytes: number;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Conditionally revalidate `domain.json` by ETag when intrinsic release discovery is unavailable.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated Use `ReleaseDiscovery`.
|
|
16
|
+
*/
|
|
12
17
|
export declare class PublicationDiscovery {
|
|
13
18
|
private readonly options;
|
|
14
19
|
private publication;
|
|
@@ -19,5 +24,3 @@ export declare class PublicationDiscovery {
|
|
|
19
24
|
invalidate(): void;
|
|
20
25
|
private fetch;
|
|
21
26
|
}
|
|
22
|
-
export declare function boundedResponse(response: Response, maximum: number, signal: AbortSignal): Promise<Uint8Array>;
|
|
23
|
-
export declare function requireExactResponse(response: Response, expected: string, label: string): void;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
import * as publicationCodec from '@astrale-os/kernel-protocol/publication/publication';
|
|
3
|
-
import { ClientError } from '
|
|
4
|
-
import { allowEndpoint } from '
|
|
5
|
-
import {
|
|
3
|
+
import { ClientError } from '../../../errors/index.js';
|
|
4
|
+
import { allowEndpoint } from '../../context/index.js';
|
|
5
|
+
import { boundedResponse, requireExactResponse } from '../response.js';
|
|
6
|
+
import { runAcquisition } from '../transport.js';
|
|
7
|
+
/**
|
|
8
|
+
* Conditionally revalidate `domain.json` by ETag when intrinsic release discovery is unavailable.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `ReleaseDiscovery`.
|
|
11
|
+
*/
|
|
6
12
|
export class PublicationDiscovery {
|
|
7
13
|
options;
|
|
8
14
|
publication;
|
|
@@ -54,6 +60,10 @@ export class PublicationDiscovery {
|
|
|
54
60
|
}
|
|
55
61
|
const body = await runAcquisition(operation, 'publication', 'receive', 'Kernel Publication response could not be read.', (signal) => boundedResponse(response, this.options.maximumBytes, signal));
|
|
56
62
|
const admitted = publicationCodec.accept(codecs.json.decode(body));
|
|
63
|
+
// Version 2 is only a retained Kernel-side format; a live Kernel serves version 3.
|
|
64
|
+
if (admitted.version !== 3) {
|
|
65
|
+
throw new ClientError('Kernel Publication version 2 is no longer supported; the Kernel must serve Publication version 3.');
|
|
66
|
+
}
|
|
57
67
|
if (admitted.identity.issuer !== this.options.kernel) {
|
|
58
68
|
throw new ClientError('Publication issuer differs from the requested Kernel issuer.');
|
|
59
69
|
}
|
|
@@ -64,60 +74,3 @@ export class PublicationDiscovery {
|
|
|
64
74
|
return admitted;
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
|
-
export async function boundedResponse(response, maximum, signal) {
|
|
68
|
-
const declared = response.headers.get('content-length');
|
|
69
|
-
if (declared !== null) {
|
|
70
|
-
const length = Number(declared);
|
|
71
|
-
if (!Number.isSafeInteger(length) || length < 0) {
|
|
72
|
-
throw new ClientError('Response Content-Length is invalid.');
|
|
73
|
-
}
|
|
74
|
-
if (length > maximum)
|
|
75
|
-
throw new ClientError('Response exceeds its byte limit.');
|
|
76
|
-
}
|
|
77
|
-
if (response.body === null)
|
|
78
|
-
return new Uint8Array();
|
|
79
|
-
const reader = response.body.getReader();
|
|
80
|
-
const chunks = [];
|
|
81
|
-
let total = 0;
|
|
82
|
-
const abort = () => void reader.cancel(signal.reason).catch(() => undefined);
|
|
83
|
-
if (signal.aborted)
|
|
84
|
-
abort();
|
|
85
|
-
else
|
|
86
|
-
signal.addEventListener('abort', abort, { once: true });
|
|
87
|
-
try {
|
|
88
|
-
while (true) {
|
|
89
|
-
if (signal.aborted)
|
|
90
|
-
throw signal.reason;
|
|
91
|
-
const next = await reader.read();
|
|
92
|
-
if (next.done)
|
|
93
|
-
break;
|
|
94
|
-
total += next.value.byteLength;
|
|
95
|
-
if (total > maximum) {
|
|
96
|
-
await reader.cancel('response exceeds byte limit').catch(() => undefined);
|
|
97
|
-
throw new ClientError('Response exceeds its byte limit.');
|
|
98
|
-
}
|
|
99
|
-
chunks.push(next.value);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
signal.removeEventListener('abort', abort);
|
|
104
|
-
reader.releaseLock();
|
|
105
|
-
}
|
|
106
|
-
return join(chunks, total);
|
|
107
|
-
}
|
|
108
|
-
export function requireExactResponse(response, expected, label) {
|
|
109
|
-
if (response.redirected || response.url !== expected) {
|
|
110
|
-
throw new ClientError(`${label} did not return the exact requested URL.`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function join(parts, length) {
|
|
114
|
-
if (parts.length === 1)
|
|
115
|
-
return new Uint8Array(parts[0]);
|
|
116
|
-
const output = new Uint8Array(length);
|
|
117
|
-
let offset = 0;
|
|
118
|
-
for (const part of parts) {
|
|
119
|
-
output.set(part, offset);
|
|
120
|
-
offset += part.byteLength;
|
|
121
|
-
}
|
|
122
|
-
return output;
|
|
123
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
|
+
import type { release } from '@astrale-os/kernel-protocol';
|
|
3
|
+
import type { Fetch } from '../../transport/http/index.js';
|
|
4
|
+
import type { SessionOperation } from '../context/index.js';
|
|
5
|
+
import { type SessionPolicy } from '../context/index.js';
|
|
6
|
+
/** The source Kernel's release, or its version 3 Publication when `release.json` is unavailable. */
|
|
7
|
+
export type IntrinsicRelease = release.DomainRelease | release.legacy.Publication;
|
|
8
|
+
export interface ReleaseDiscoveryOptions {
|
|
9
|
+
readonly kernel: IssuerId;
|
|
10
|
+
readonly fetch: Fetch;
|
|
11
|
+
readonly policy: Readonly<SessionPolicy>;
|
|
12
|
+
readonly maximumBytes: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Conditionally revalidate by release digest. Only an exact 404 means absence;
|
|
16
|
+
* all other unsuccessful responses and Fetch failures remain errors.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ReleaseDiscovery {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private release;
|
|
21
|
+
private revision;
|
|
22
|
+
constructor(options: ReleaseDiscoveryOptions);
|
|
23
|
+
/** The release admitted since construction, the last `invalidate()` or the last 404, if any. */
|
|
24
|
+
current(): release.DomainRelease | undefined;
|
|
25
|
+
/** The admitted release, or undefined when the Host answers `release.json` with its exact 404. */
|
|
26
|
+
revalidate(operation: SessionOperation): Promise<release.DomainRelease | undefined>;
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
private fetch;
|
|
29
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
|
+
import * as releaseCodec from '@astrale-os/kernel-protocol/release';
|
|
3
|
+
import { ClientError } from '../../errors/index.js';
|
|
4
|
+
import { allowEndpoint } from '../context/index.js';
|
|
5
|
+
import { boundedResponse, requireExactResponse } from './response.js';
|
|
6
|
+
import { runAcquisition } from './transport.js';
|
|
7
|
+
/**
|
|
8
|
+
* Conditionally revalidate by release digest. Only an exact 404 means absence;
|
|
9
|
+
* all other unsuccessful responses and Fetch failures remain errors.
|
|
10
|
+
*/
|
|
11
|
+
export class ReleaseDiscovery {
|
|
12
|
+
options;
|
|
13
|
+
release;
|
|
14
|
+
revision = 0;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
/** The release admitted since construction, the last `invalidate()` or the last 404, if any. */
|
|
19
|
+
current() {
|
|
20
|
+
return this.release;
|
|
21
|
+
}
|
|
22
|
+
/** The admitted release, or undefined when the Host answers `release.json` with its exact 404. */
|
|
23
|
+
async revalidate(operation) {
|
|
24
|
+
const revision = this.revision;
|
|
25
|
+
const previous = this.release;
|
|
26
|
+
const discovered = await this.fetch(operation, previous);
|
|
27
|
+
if (revision !== this.revision)
|
|
28
|
+
return this.revalidate(operation);
|
|
29
|
+
const retained = discovered !== undefined && previous?.digest === discovered.digest ? previous : discovered;
|
|
30
|
+
this.release = retained;
|
|
31
|
+
return retained;
|
|
32
|
+
}
|
|
33
|
+
invalidate() {
|
|
34
|
+
this.revision += 1;
|
|
35
|
+
this.release = undefined;
|
|
36
|
+
}
|
|
37
|
+
async fetch(operation, previous) {
|
|
38
|
+
const url = releaseCodec.url(this.options.kernel);
|
|
39
|
+
allowEndpoint(url, this.options.policy);
|
|
40
|
+
const response = await runAcquisition(operation, 'release', 'unknown', 'Kernel release could not be retrieved.', (signal) => this.options.fetch(url, {
|
|
41
|
+
headers: {
|
|
42
|
+
accept: releaseCodec.MEDIA_TYPE,
|
|
43
|
+
...(previous === undefined ? {} : { 'if-none-match': `"${previous.digest}"` }),
|
|
44
|
+
},
|
|
45
|
+
redirect: 'manual',
|
|
46
|
+
signal,
|
|
47
|
+
}));
|
|
48
|
+
requireExactResponse(response, url, 'Release discovery');
|
|
49
|
+
if (response.status === 404) {
|
|
50
|
+
// Cleanup must not hold discovery open on an injected body cancellation.
|
|
51
|
+
void response.body?.cancel().catch(() => undefined);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (response.status === 304) {
|
|
55
|
+
if (previous === undefined) {
|
|
56
|
+
throw new ClientError('Release discovery returned an unexpected not-modified response.');
|
|
57
|
+
}
|
|
58
|
+
const etag = response.headers.get('etag');
|
|
59
|
+
if (etag !== null && etag !== `"${previous.digest}"`) {
|
|
60
|
+
throw new ClientError('Release not-modified ETag differs from the retained release digest.');
|
|
61
|
+
}
|
|
62
|
+
return previous;
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new ClientError(`Release discovery returned HTTP ${response.status}.`);
|
|
66
|
+
}
|
|
67
|
+
if (response.headers.get('content-type') !== releaseCodec.MEDIA_TYPE) {
|
|
68
|
+
throw new ClientError('Release response media type differs from the DomainRelease v4 media type.');
|
|
69
|
+
}
|
|
70
|
+
const body = await runAcquisition(operation, 'release', 'receive', 'Kernel release response could not be read.', (signal) => boundedResponse(response, this.options.maximumBytes, signal));
|
|
71
|
+
const admitted = releaseCodec.accept(codecs.json.decode(body));
|
|
72
|
+
if (admitted.identity.issuer !== this.options.kernel) {
|
|
73
|
+
throw new ClientError('Release issuer differs from the requested Kernel issuer.');
|
|
74
|
+
}
|
|
75
|
+
const etag = response.headers.get('etag');
|
|
76
|
+
if (etag !== null && etag !== `"${admitted.digest}"`) {
|
|
77
|
+
throw new ClientError('Release response ETag differs from its release digest.');
|
|
78
|
+
}
|
|
79
|
+
return admitted;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Read external bytes within one Session operation's byte and lifetime bounds. */
|
|
2
|
+
export declare function boundedResponse(response: Response, maximum: number, signal: AbortSignal): Promise<Uint8Array>;
|
|
3
|
+
export declare function requireExactResponse(response: Response, expected: string, label: string): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ClientError } from '../../errors/index.js';
|
|
2
|
+
/** Read external bytes within one Session operation's byte and lifetime bounds. */
|
|
3
|
+
export async function boundedResponse(response, maximum, signal) {
|
|
4
|
+
const declared = response.headers.get('content-length');
|
|
5
|
+
if (declared !== null) {
|
|
6
|
+
const length = Number(declared);
|
|
7
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
8
|
+
throw new ClientError('Response Content-Length is invalid.');
|
|
9
|
+
}
|
|
10
|
+
if (length > maximum)
|
|
11
|
+
throw new ClientError('Response exceeds its byte limit.');
|
|
12
|
+
}
|
|
13
|
+
if (response.body === null)
|
|
14
|
+
return new Uint8Array();
|
|
15
|
+
const reader = response.body.getReader();
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let total = 0;
|
|
18
|
+
const abort = () => void reader.cancel(signal.reason).catch(() => undefined);
|
|
19
|
+
if (signal.aborted)
|
|
20
|
+
abort();
|
|
21
|
+
else
|
|
22
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
23
|
+
try {
|
|
24
|
+
while (true) {
|
|
25
|
+
if (signal.aborted)
|
|
26
|
+
throw signal.reason;
|
|
27
|
+
const next = await reader.read();
|
|
28
|
+
if (next.done)
|
|
29
|
+
break;
|
|
30
|
+
total += next.value.byteLength;
|
|
31
|
+
if (total > maximum) {
|
|
32
|
+
await reader.cancel('response exceeds byte limit').catch(() => undefined);
|
|
33
|
+
throw new ClientError('Response exceeds its byte limit.');
|
|
34
|
+
}
|
|
35
|
+
chunks.push(next.value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
signal.removeEventListener('abort', abort);
|
|
40
|
+
reader.releaseLock();
|
|
41
|
+
}
|
|
42
|
+
return join(chunks, total);
|
|
43
|
+
}
|
|
44
|
+
export function requireExactResponse(response, expected, label) {
|
|
45
|
+
if (response.redirected || response.url !== expected) {
|
|
46
|
+
throw new ClientError(`${label} did not return the exact requested URL.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function join(parts, length) {
|
|
50
|
+
if (parts.length === 1)
|
|
51
|
+
return new Uint8Array(parts[0]);
|
|
52
|
+
const output = new Uint8Array(length);
|
|
53
|
+
let offset = 0;
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
output.set(part, offset);
|
|
56
|
+
offset += part.byteLength;
|
|
57
|
+
}
|
|
58
|
+
return output;
|
|
59
|
+
}
|
|
@@ -2,11 +2,18 @@ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
|
2
2
|
import type { bundle } from '@astrale-os/kernel-dsl/v1';
|
|
3
3
|
import type { publication } from '@astrale-os/kernel-protocol';
|
|
4
4
|
import type { ViewCatalog } from '../views/index.js';
|
|
5
|
-
|
|
5
|
+
import type { IntrinsicRelease } from './release.js';
|
|
6
|
+
/** Coherent public observation acquired through one Client Session at one intrinsic release. */
|
|
6
7
|
export interface SessionSnapshot {
|
|
7
8
|
readonly kernel: IssuerId;
|
|
9
|
+
readonly release: IntrinsicRelease;
|
|
10
|
+
/**
|
|
11
|
+
* Bundle and root View provenance use this v3 document's ETag.
|
|
12
|
+
*
|
|
13
|
+
* @deprecated Use `release`.
|
|
14
|
+
*/
|
|
8
15
|
readonly publication: publication.Publication;
|
|
9
16
|
readonly bundle: bundle.Bundle;
|
|
10
17
|
readonly views: ViewCatalog;
|
|
11
18
|
}
|
|
12
|
-
export declare function createSessionSnapshot(kernel: IssuerId, publication: publication.Publication, artifact: bundle.Bundle, views: ViewCatalog): SessionSnapshot;
|
|
19
|
+
export declare function createSessionSnapshot(kernel: IssuerId, release: IntrinsicRelease, publication: publication.Publication, artifact: bundle.Bundle, views: ViewCatalog): SessionSnapshot;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export function createSessionSnapshot(kernel, publication, artifact, views) {
|
|
2
|
-
return Object.freeze({ kernel, publication, bundle: artifact, views });
|
|
1
|
+
export function createSessionSnapshot(kernel, release, publication, artifact, views) {
|
|
2
|
+
return Object.freeze({ kernel, release, publication, bundle: artifact, views });
|
|
3
3
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { TransportPhase } from '../../errors/index.js';
|
|
1
|
+
import type { AcquisitionResource, TransportPhase } from '../../errors/index.js';
|
|
2
2
|
import type { SessionOperation } from '../context/index.js';
|
|
3
|
-
type AcquisitionResource = 'publication' | 'bundle';
|
|
4
3
|
/** Normalize only unknown failures from one injected acquisition capability. */
|
|
5
4
|
export declare function runAcquisition<Result>(operation: SessionOperation, resource: AcquisitionResource, phase: Extract<TransportPhase, 'receive' | 'unknown'>, message: string, task: (signal: AbortSignal) => Result | PromiseLike<Result>): Promise<Result>;
|
|
6
|
-
export {};
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { type IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
2
|
import type { SessionControlOptions } from '../context/index.js';
|
|
3
3
|
import type { SessionAuth } from '../routing/index.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
readonly mode?: 'auto' | 'source';
|
|
4
|
+
/**
|
|
5
|
+
* Choose how authenticated Calls to remote Domains present the Session's source authority: `auto`
|
|
6
|
+
* exchanges through each callable's declaring Domain; `source` keeps the source credential.
|
|
7
|
+
*/
|
|
8
|
+
export type SessionExchangeOptions = {
|
|
9
|
+
readonly mode: 'auto';
|
|
11
10
|
/**
|
|
12
11
|
* Delegation lifetime requested for each Domain credential. The source credential's remaining
|
|
13
|
-
* lifetime and the Domain's own ceiling may shorten it.
|
|
12
|
+
* lifetime and the Domain's own ceiling may shorten it.
|
|
14
13
|
*/
|
|
15
14
|
readonly ttlSeconds?: number;
|
|
16
|
-
}
|
|
15
|
+
} | {
|
|
16
|
+
readonly mode: 'source';
|
|
17
|
+
};
|
|
17
18
|
/** One explicit exchange through a named Domain issuer. */
|
|
18
19
|
export interface SessionExchangeRequest extends SessionControlOptions {
|
|
19
20
|
/** Requested delegation lifetime; the source credential and the Domain may shorten it. */
|
|
@@ -29,6 +30,8 @@ export interface ExchangeRequest {
|
|
|
29
30
|
* Admit exchange options against the Session auth they extend. Every Domain credential must still
|
|
30
31
|
* cover the Session delegation, so a request that could never cover it is rejected.
|
|
31
32
|
*/
|
|
32
|
-
export declare function admitExchangeOptions(input: SessionExchangeOptions | undefined, auth: SessionAuth | undefined): Readonly<
|
|
33
|
+
export declare function admitExchangeOptions(input: SessionExchangeOptions | undefined, auth: SessionAuth | undefined): Readonly<{
|
|
34
|
+
ttlSeconds: number;
|
|
35
|
+
}> | undefined;
|
|
33
36
|
/** Admit an explicit exchange: the canonical absolute issuer URL of the named Domain and its lifetime. */
|
|
34
37
|
export declare function admitExchangeRequest(domain: string, input: SessionExchangeRequest | undefined): ExchangeRequest;
|
|
@@ -12,14 +12,15 @@ export function admitExchangeOptions(input, auth) {
|
|
|
12
12
|
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
13
13
|
throw new TypeError('Session exchange options are invalid.');
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
throw new TypeError('Session exchange options contain unexpected fields.');
|
|
17
|
-
}
|
|
18
|
-
const mode = input.mode === undefined ? 'auto' : input.mode;
|
|
15
|
+
const mode = input.mode;
|
|
19
16
|
if (mode !== 'auto' && mode !== 'source') {
|
|
20
17
|
throw new TypeError('Session exchange mode is invalid.');
|
|
21
18
|
}
|
|
22
|
-
|
|
19
|
+
const fields = mode === 'auto' ? ['mode', 'ttlSeconds'] : ['mode'];
|
|
20
|
+
if (Reflect.ownKeys(input).some((key) => typeof key !== 'string' || !fields.includes(key))) {
|
|
21
|
+
throw new TypeError('Session exchange options contain unexpected fields.');
|
|
22
|
+
}
|
|
23
|
+
if (input.mode === 'source')
|
|
23
24
|
return undefined;
|
|
24
25
|
if (auth === undefined)
|
|
25
26
|
throw new TypeError('Session exchange requires Session auth.');
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AuthExchange, SessionAuth } from '../routing/index.js';
|
|
2
2
|
import type { ExchangeDependencies } from './exchange.js';
|
|
3
|
-
import type { SessionExchangeOptions } from './options.js';
|
|
4
3
|
/** The per-Call exchange a Session applies after resolving its source authority. */
|
|
5
4
|
export interface SessionExchange extends AuthExchange {
|
|
6
5
|
clear(): void;
|
|
@@ -15,4 +14,6 @@ export interface SessionExchange extends AuthExchange {
|
|
|
15
14
|
*/
|
|
16
15
|
export declare function createSessionExchange(dependencies: ExchangeDependencies & {
|
|
17
16
|
readonly auth: SessionAuth;
|
|
18
|
-
}, options: Readonly<
|
|
17
|
+
}, options: Readonly<{
|
|
18
|
+
ttlSeconds: number;
|
|
19
|
+
}>): SessionExchange;
|
package/dist/session/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type { SessionControlOptions, SessionPolicy } from './context/index.js';
|
|
|
2
2
|
export type { ContentApi, ContentLocation, ContentUploadOptions, ContentUploadResult, } from './content.js';
|
|
3
3
|
export { bindSession, type BoundClientSession, type BoundSessionControls, type BoundSessionSource, } from './bound.js';
|
|
4
4
|
export { createSessionCredentialProvider, type SessionCredential, type SessionCredentialProvider, type SessionCredentialProviderOptions, type SessionCredentialState, } from './credential/index.js';
|
|
5
|
-
export type { SessionSnapshot } from './discovery/index.js';
|
|
5
|
+
export type { IntrinsicRelease, SessionSnapshot } from './discovery/index.js';
|
|
6
6
|
export type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
7
7
|
export type { Auth, PersistedRoute, SessionRouteArtifact, SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody, } from './routing/index.js';
|
|
8
8
|
export { ClientSession, connect, type ClientSessionOptions, type ConnectOptions, } from './session.js';
|