@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.
Files changed (61) hide show
  1. package/dist/client/admission.js +1 -9
  2. package/dist/client/client.js +3 -1
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/routing-control.d.ts +11 -3
  6. package/dist/client/routing-control.js +18 -3
  7. package/dist/errors/stale-route-error.d.ts +14 -3
  8. package/dist/errors/stale-route-error.js +15 -3
  9. package/dist/errors/transport-error.d.ts +3 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/schema/index.d.ts +1 -1
  12. package/dist/schema/introspection.js +10 -6
  13. package/dist/schema/schema.d.ts +3 -0
  14. package/dist/schema/schema.js +13 -2
  15. package/dist/session/bound.js +10 -1
  16. package/dist/session/content.js +4 -27
  17. package/dist/session/context/index.d.ts +1 -1
  18. package/dist/session/context/index.js +1 -1
  19. package/dist/session/context/limits.d.ts +1 -0
  20. package/dist/session/context/limits.js +1 -0
  21. package/dist/session/discovery/bundle.js +1 -1
  22. package/dist/session/discovery/discovery.d.ts +12 -4
  23. package/dist/session/discovery/discovery.js +28 -19
  24. package/dist/session/discovery/index.d.ts +8 -1
  25. package/dist/session/discovery/index.js +5 -8
  26. package/dist/session/discovery/legacy/index.d.ts +38 -0
  27. package/dist/session/discovery/legacy/index.js +38 -0
  28. package/dist/session/discovery/legacy/intrinsic.d.ts +33 -0
  29. package/dist/session/discovery/legacy/intrinsic.js +67 -0
  30. package/dist/session/discovery/legacy/projection.d.ts +8 -0
  31. package/dist/session/discovery/legacy/projection.js +17 -0
  32. package/dist/session/discovery/{publication.d.ts → legacy/publication.d.ts} +8 -5
  33. package/dist/session/discovery/{publication.js → legacy/publication.js} +13 -60
  34. package/dist/session/discovery/release.d.ts +29 -0
  35. package/dist/session/discovery/release.js +81 -0
  36. package/dist/session/discovery/response.d.ts +3 -0
  37. package/dist/session/discovery/response.js +59 -0
  38. package/dist/session/discovery/snapshot.d.ts +9 -2
  39. package/dist/session/discovery/snapshot.js +2 -2
  40. package/dist/session/discovery/transport.d.ts +1 -3
  41. package/dist/session/exchange/options.d.ts +13 -10
  42. package/dist/session/exchange/options.js +6 -5
  43. package/dist/session/exchange/presentation.d.ts +3 -2
  44. package/dist/session/index.d.ts +1 -1
  45. package/dist/session/reference.d.ts +3 -6
  46. package/dist/session/reference.js +0 -4
  47. package/dist/session/routing/dispatch.js +36 -37
  48. package/dist/session/session.d.ts +18 -3
  49. package/dist/session/session.js +21 -10
  50. package/dist/session/session.typecheck.js +14 -3
  51. package/dist/transport/auto/transport.d.ts +0 -1
  52. package/dist/transport/auto/transport.js +0 -2
  53. package/dist/transport/http/response.d.ts +1 -1
  54. package/dist/transport/http/response.js +2 -6
  55. package/dist/transport/http/transport.d.ts +5 -3
  56. package/dist/transport/http/transport.js +40 -19
  57. package/dist/transport/index.d.ts +1 -1
  58. package/dist/transport/transport.d.ts +10 -2
  59. package/dist/transport/websocket/transport.d.ts +0 -3
  60. package/dist/transport/websocket/transport.js +1 -16
  61. package/package.json +6 -7
@@ -1,6 +1,6 @@
1
1
  import type { Path as PathValue, PathLike } from '@astrale-os/kernel-core/path';
2
2
  import type { CallableInputOf, CallableInputModeOf, CallableOutputOf, CallableResultOf, Domain, ResolvedFunction, ExecutableMethod } from '@astrale-os/kernel-dsl/v1';
3
- import type { MethodAddress, Revision } from '@astrale-os/kernel-dsl/v1/addressing';
3
+ import type { MethodAddress } from '@astrale-os/kernel-dsl/v1/addressing';
4
4
  import type { MeaningFingerprint } from '@astrale-os/kernel-dsl/v1/schema/fingerprint';
5
5
  import { type Key as KeyType } from '@astrale-os/kernel-dsl/v1/addressing';
6
6
  import type { Binary, Input } from '../client/index.js';
@@ -10,8 +10,6 @@ declare const REFERENCE_INPUT_MODE: unique symbol;
10
10
  declare const REFERENCE_OUTPUT: unique symbol;
11
11
  export interface CallableReference<InputValue = Input, Output = unknown, InputMode extends 'value' | 'binary' = 'value' | 'binary'> {
12
12
  readonly origin: KeyType.Origin;
13
- /** @deprecated Informational only; dispatch pins `fingerprint` and revision pinning will be removed. */
14
- readonly revision?: Revision;
15
13
  /** Meaning fingerprint of the callable; a generated reference always carries one. */
16
14
  readonly fingerprint?: MeaningFingerprint;
17
15
  readonly callable: KeyType.Callable;
@@ -24,7 +22,6 @@ export interface CallableReference<InputValue = Input, Output = unknown, InputMo
24
22
  export interface InstanceCallableReference<InputValue = Input, Output = unknown> {
25
23
  (receiver: PathLike): CallableReference<InputValue, Output>;
26
24
  readonly origin: KeyType.Origin;
27
- readonly revision?: Revision;
28
25
  readonly fingerprint?: MeaningFingerprint;
29
26
  readonly callable: KeyType.Callable;
30
27
  readonly outputMode: 'value' | 'stream' | 'binary';
@@ -42,9 +39,9 @@ type ReferenceOutput<Callable> = Callable extends {
42
39
  type ExecutableStaticMethod = ExecutableMethod<unknown, unknown, true>;
43
40
  type ExecutableInstanceMethod = ExecutableMethod<unknown, unknown, false>;
44
41
  /** Project one exact resolved Function or static Method into a typed Client reference. */
45
- export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>, CallableInputModeOf<Callable>>;
42
+ export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>, CallableInputModeOf<Callable>>;
46
43
  /** Project one exact resolved instance Method into a receiver-bound typed Client reference. */
47
- export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): InstanceCallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
44
+ export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin'>, callable: Callable): InstanceCallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
48
45
  /**
49
46
  * Reference a Kernel instance Method by its Core coordinate, without a meaning pin, as the Session
50
47
  * addresses every Kernel syscall. Loading the Session therefore never resolves the Kernel Schema.
@@ -9,7 +9,6 @@ export function reference(domain, callable) {
9
9
  invalid();
10
10
  const common = Object.freeze({
11
11
  origin: domain.origin,
12
- revision: domain.revision,
13
12
  fingerprint: callableFingerprint(callable),
14
13
  callable: callable.key,
15
14
  outputMode: callable.output.mode,
@@ -45,8 +44,6 @@ export function admitReference(input) {
45
44
  invalid();
46
45
  if (typeof input.origin !== 'string' ||
47
46
  !patterns.origin.test(input.origin) ||
48
- (input.revision !== undefined &&
49
- (typeof input.revision !== 'string' || !patterns.revision.test(input.revision))) ||
50
47
  typeof input.callable !== 'string' ||
51
48
  !Key.is(input.callable) ||
52
49
  Key.origin(input.callable) !== input.origin ||
@@ -59,7 +56,6 @@ export function admitReference(input) {
59
56
  invalid();
60
57
  return Object.freeze({
61
58
  origin: input.origin,
62
- ...(input.revision === undefined ? {} : { revision: input.revision }),
63
59
  ...(input.fingerprint === undefined ? {} : { fingerprint: input.fingerprint }),
64
60
  callable: input.callable,
65
61
  target,
@@ -1,6 +1,6 @@
1
1
  import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
2
2
  import * as invocationProtocol from '@astrale-os/kernel-protocol/invocation';
3
- import { acceptCall, Client, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
3
+ import { acceptCall, Client, dispatchTrace, isAdmittedDeploymentRetiredError, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
4
4
  import { ClientError, ResponseError } from '../../errors/index.js';
5
5
  import { runSessionOperation, remainingSessionTime, sessionTransportOptions, allowEndpoint, } from '../context/index.js';
6
6
  import { resolveAuth } from './auth.js';
@@ -86,23 +86,23 @@ export class SessionDispatcher {
86
86
  return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace, streaming);
87
87
  }
88
88
  catch (cause) {
89
- const stale = isAdmittedStaleRouteError(cause);
90
- const miss = isAdmittedMissError(cause);
91
- if (isRouteCompatibilityConflict(cause)) {
92
- this.state.cache.delete(state.cacheKey);
89
+ const control = routingControl(cause);
90
+ if (control === undefined) {
91
+ if (isRouteCompatibilityConflict(cause))
92
+ this.state.cache.delete(state.cacheKey);
93
93
  throw cause;
94
94
  }
95
- if (!stale && !miss)
96
- throw cause;
97
95
  this.state.cache.delete(state.cacheKey);
98
96
  if (!operation.claimRecovery())
99
97
  throw cause;
100
- const routing = stale
101
- ? Object.freeze({
98
+ // A stale or retired route names the release the destination no longer serves; a miss
99
+ // names a Path, so the source receives no digest for it.
100
+ const routing = control === 'miss'
101
+ ? undefined
102
+ : Object.freeze({
102
103
  kind: 'refresh',
103
- stale: learned.route.via.publication.etag,
104
- })
105
- : undefined;
104
+ stale: invocationProtocol.routedDigest(learned.route.via),
105
+ });
106
106
  const refreshed = await this.dispatchSource(state, routing, options, operation, streaming);
107
107
  return this.followSource(refreshed, state, options, operation, streaming);
108
108
  }
@@ -129,30 +129,16 @@ export class SessionDispatcher {
129
129
  ...sessionTransportOptions(operation),
130
130
  routing: learned.route.via,
131
131
  };
132
- // The source has admitted this fingerprint against the exact Publication carried by Via.
133
- // Pin that revision at the destination so existing Workers can execute the same admitted
134
- // callable without understanding the newer fingerprint Target representation. The cache
135
- // remains partitioned by the original fingerprint, and stale recovery re-admits it at source.
136
- const destinationCall = typeof call.target !== 'string' && call.target.fingerprint !== undefined
137
- ? {
138
- ...call,
139
- target: invocationProtocol.Target({
140
- callable: call.target.callable,
141
- revision: learned.route.via.publication.revision,
142
- ...(call.target.instance === undefined ? {} : { instance: call.target.instance }),
143
- }),
144
- }
145
- : call;
146
132
  const result = await runSessionOperation(operation, () => streaming?.body !== undefined
147
133
  ? trace === undefined
148
- ? authenticated(client, learned.credential).dispatch(destinationCall, {
134
+ ? authenticated(client, learned.credential).dispatch(call, {
149
135
  ...binaryRequestOptions,
150
136
  body: streaming.body,
151
137
  })
152
- : dispatchTrace(client, learned.credential, destinationCall, { ...binaryRequestOptions, body: streaming.body }, trace)
138
+ : dispatchTrace(client, learned.credential, call, { ...binaryRequestOptions, body: streaming.body }, trace)
153
139
  : trace === undefined
154
- ? authenticated(client, learned.credential).dispatch(destinationCall, requestOptions)
155
- : dispatchTrace(client, learned.credential, destinationCall, requestOptions, trace));
140
+ ? authenticated(client, learned.credential).dispatch(call, requestOptions)
141
+ : dispatchTrace(client, learned.credential, call, requestOptions, trace));
156
142
  if (result.kind === 'redirect') {
157
143
  throw new ClientError('A destination redirect is invalid; only the source Kernel may redirect.');
158
144
  }
@@ -177,6 +163,20 @@ function requireCachedRoute(learned, source, target, envelopeTransport, websocke
177
163
  requireTargetPublication(learned.route, target);
178
164
  selectEnvelopeTarget(learned.route.endpoint, envelopeTransport, websocketAvailable, policy);
179
165
  }
166
+ /** The privately witnessed pre-handler control a destination failure carries, if any. */
167
+ function routingControl(input) {
168
+ if (isAdmittedStaleRouteError(input))
169
+ return 'stale';
170
+ if (isAdmittedMissError(input))
171
+ return 'miss';
172
+ if (isAdmittedDeploymentRetiredError(input))
173
+ return 'retired';
174
+ return undefined;
175
+ }
176
+ /**
177
+ * A failure that proves the warm route can no longer serve this call without witnessing a
178
+ * pre-handler control: the route is evicted for later calls, but the invocation is never replayed.
179
+ */
180
180
  function isRouteCompatibilityConflict(input) {
181
181
  if (!(input instanceof ResponseError))
182
182
  return false;
@@ -185,17 +185,16 @@ function isRouteCompatibilityConflict(input) {
185
185
  message: input.message,
186
186
  ...(input.data === undefined ? {} : { data: input.data }),
187
187
  });
188
- return (failure?.data.code === invocationProtocol.SCHEMA_REVISION_MISMATCH ||
189
- failure?.data.code === invocationProtocol.CALLABLE_FINGERPRINT_MISMATCH ||
190
- failure?.data.code === invocationProtocol.SERVICE_TRUST_REQUIRED);
188
+ return (failure?.data.code === invocationProtocol.CALLABLE_FINGERPRINT_MISMATCH ||
189
+ failure?.data.code === invocationProtocol.SERVICE_TRUST_REQUIRED ||
190
+ failure?.data.code === invocationProtocol.DEPLOYMENT_RETIRED);
191
191
  }
192
192
  function requireTargetPublication(route, target) {
193
193
  if (typeof target === 'string')
194
194
  return;
195
- const actual = route.via.publication;
196
- if (actual.origin !== Key.origin(target.callable) ||
197
- (target.revision !== undefined && actual.revision !== target.revision)) {
198
- throw new ClientError('Resolved route does not match the expected Schema revision.');
195
+ const actual = 'release' in route.via ? route.via.release : route.via.publication;
196
+ if (actual.origin !== Key.origin(target.callable)) {
197
+ throw new ClientError('Resolved route does not match the Target origin.');
199
198
  }
200
199
  }
201
200
  export function requireResultKind(expected, result) {
@@ -13,7 +13,7 @@ import type { WebSocketConstructor } from '../transport/websocket/index.js';
13
13
  import type { ContentApi } from './content.js';
14
14
  import type { SessionControlOptions, SessionPolicy } from './context/index.js';
15
15
  import type { SessionCredential } from './credential/index.js';
16
- import type { SessionSnapshot } from './discovery/index.js';
16
+ import type { IntrinsicRelease, SessionSnapshot } from './discovery/index.js';
17
17
  import type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
18
18
  import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
19
19
  import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
@@ -31,7 +31,6 @@ export interface ClientSessionOptions {
31
31
  * caller, so the Domain is the principal and the caller is `auth`. Kernel and local callables keep
32
32
  * the `auth` credential. `connect` defaults to `auto` when `auth` is present; `new ClientSession`
33
33
  * keeps source authority when this option is omitted. `{ mode: 'source' }` keeps it for every Call.
34
- * Legacy `{}` and `{ ttlSeconds }` options remain equivalent to `{ mode: 'auto' }`.
35
34
  */
36
35
  readonly exchange?: SessionExchangeOptions;
37
36
  /** Optional caller-owned persistence for bounded confidential route artifacts. */
@@ -39,6 +38,12 @@ export interface ClientSessionOptions {
39
38
  readonly policy: SessionPolicy;
40
39
  readonly envelopeTransport?: SessionEnvelopeTransport;
41
40
  readonly onWarning?: (warning: ClientWarning) => void;
41
+ /** Bounds the source Kernel's own `release.json`. */
42
+ readonly maximumReleaseBytes?: number;
43
+ /**
44
+ * Bounds every Publication the Session reads: the version 3 `domain.json` a Host that predates
45
+ * `release.json` serves, and the Publications Schema results carry.
46
+ */
42
47
  readonly maximumPublicationBytes?: number;
43
48
  readonly maximumBundleBytes?: number;
44
49
  readonly maximumResponseBytes?: number;
@@ -75,7 +80,17 @@ export declare class ClientSession {
75
80
  * `createSessionCredentialProvider`, for as long as it covers the calls it carries.
76
81
  */
77
82
  exchange(domain: string, options?: SessionExchangeRequest): Promise<SessionCredential>;
78
- /** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
83
+ /**
84
+ * Revalidate the source Kernel's own release (`release.json`) without acquiring its bundle or
85
+ * Views; a Host that predates intrinsic `release.json` yields its version 3 Publication instead.
86
+ */
87
+ release(options?: SessionControlOptions): Promise<IntrinsicRelease>;
88
+ /**
89
+ * Revalidate the intrinsic release and return its v3 document: the Host's own version 3
90
+ * Publication, or the frozen v3 projection of its `DomainRelease`.
91
+ *
92
+ * @deprecated Use `release()`. Deleted when no supported Host predates intrinsic `release.json`.
93
+ */
79
94
  publication(options?: SessionControlOptions): Promise<publication.Publication>;
80
95
  snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
81
96
  viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
@@ -12,7 +12,7 @@ import { HttpTransport } from '../transport/http/index.js';
12
12
  import { WebSocketTransport } from '../transport/websocket/index.js';
13
13
  import { admitFunction } from './admission.js';
14
14
  import { createContentApi } from './content.js';
15
- import { admitControlOptions, admitPolicy, createSessionOperation, DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_TIMEOUT_MS, admitKernel, allowEndpoint, kernelInvocationEndpoint, platformDuration, positive, } from './context/index.js';
15
+ import { admitControlOptions, admitPolicy, createSessionOperation, DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RELEASE_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_TIMEOUT_MS, admitKernel, allowEndpoint, kernelInvocationEndpoint, platformDuration, positive, } from './context/index.js';
16
16
  import { createSessionDiscovery } from './discovery/index.js';
17
17
  import { acquireDomainCredential, admitExchangeOptions, admitExchangeRequest, createSessionExchange, } from './exchange/index.js';
18
18
  import { admitReference } from './reference.js';
@@ -51,6 +51,7 @@ export class ClientSession {
51
51
  if (options.client !== undefined && options.pool !== undefined) {
52
52
  throw new TypeError('ClientSession accepts either client or pool, not both.');
53
53
  }
54
+ const maximumReleaseBytes = positive(options.maximumReleaseBytes ?? DEFAULT_MAXIMUM_RELEASE_BYTES, 'maximumReleaseBytes');
54
55
  const maximumPublicationBytes = positive(options.maximumPublicationBytes ?? DEFAULT_MAXIMUM_PUBLICATION_BYTES, 'maximumPublicationBytes');
55
56
  const maximumBundleBytes = positive(options.maximumBundleBytes ?? DEFAULT_MAXIMUM_BUNDLE_BYTES, 'maximumBundleBytes');
56
57
  const maximumResponseBytes = positive(options.maximumResponseBytes ?? DEFAULT_MAXIMUM_RESPONSE_BYTES, 'maximumResponseBytes');
@@ -82,6 +83,7 @@ export class ClientSession {
82
83
  kernel: this.kernel,
83
84
  fetch: fetchImpl,
84
85
  policy,
86
+ maximumReleaseBytes,
85
87
  maximumPublicationBytes,
86
88
  maximumBundleBytes,
87
89
  requireOpen: () => this.requireOpen(),
@@ -152,7 +154,19 @@ export class ClientSession {
152
154
  const request = admitExchangeRequest(domain, options);
153
155
  return acquireDomainCredential(this.exchangeDependencies, request, this.operation(request.control));
154
156
  }
155
- /** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
157
+ /**
158
+ * Revalidate the source Kernel's own release (`release.json`) without acquiring its bundle or
159
+ * Views; a Host that predates intrinsic `release.json` yields its version 3 Publication instead.
160
+ */
161
+ release(options) {
162
+ return this.discovery.release(this.operation(admitControlOptions(options)));
163
+ }
164
+ /**
165
+ * Revalidate the intrinsic release and return its v3 document: the Host's own version 3
166
+ * Publication, or the frozen v3 projection of its `DomainRelease`.
167
+ *
168
+ * @deprecated Use `release()`. Deleted when no supported Host predates intrinsic `release.json`.
169
+ */
156
170
  publication(options) {
157
171
  return this.discovery.publication(this.operation(admitControlOptions(options)));
158
172
  }
@@ -178,16 +192,14 @@ export class ClientSession {
178
192
  const instance = last?.kind === 'method' && last.dispatch === 'instance'
179
193
  ? Path.fromAST({ anchor, steps: steps.slice(0, -1) }).raw
180
194
  : undefined;
181
- // A generated reference pins the callable meaning, a hand-written one its revision, and a Kernel
182
- // reference nothing, like every other syscall the Session addresses.
195
+ // A generated reference pins the callable meaning; a Kernel reference pins nothing, like every
196
+ // other syscall the Session addresses.
183
197
  const call = {
184
198
  target: Object.freeze({
185
199
  callable: admittedReference.callable,
186
- ...(admittedReference.fingerprint !== undefined
187
- ? { fingerprint: admittedReference.fingerprint }
188
- : admittedReference.revision !== undefined
189
- ? { revision: admittedReference.revision }
190
- : {}),
200
+ ...(admittedReference.fingerprint === undefined
201
+ ? {}
202
+ : { fingerprint: admittedReference.fingerprint }),
191
203
  ...(instance === undefined ? {} : { instance }),
192
204
  }),
193
205
  input,
@@ -241,7 +253,6 @@ export class ClientSession {
241
253
  throw new TypeError(`Method owner ${method.owner.ref.origin} is absent.`);
242
254
  return this.invoke({
243
255
  origin: owner.origin,
244
- revision: owner.revision,
245
256
  fingerprint: callableFingerprint(method),
246
257
  callable: method.key,
247
258
  target: Path.instanceMethod(Path.id(node.id), method.key),
@@ -12,6 +12,9 @@ void envelope;
12
12
  void policy;
13
13
  void request;
14
14
  void snapshot;
15
+ const observedRelease = session.release(control);
16
+ void observedRelease;
17
+ void observedRelease.then((current) => current.version === 4 ? current.build.digest : current.etag);
15
18
  const observedPublication = session.publication(control);
16
19
  void observedPublication;
17
20
  const admitted = session.auth.admit({
@@ -28,11 +31,19 @@ const credentials = createSessionCredentialProvider({
28
31
  // A credential provider is a Session authentication resolver; no adapter stands between them.
29
32
  const credentialAuth = credentials;
30
33
  void new ClientSession({ ...options, auth: credentialAuth });
31
- // Automatic exchange remains compatible with the original empty and TTL-only options.
32
- void new ClientSession({ ...options, auth: credentialAuth, exchange: {} });
33
- void new ClientSession({ ...options, auth: credentialAuth, exchange: { ttlSeconds: 300 } });
34
34
  void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'auto' } });
35
+ void new ClientSession({
36
+ ...options,
37
+ auth: credentialAuth,
38
+ exchange: { mode: 'auto', ttlSeconds: 300 },
39
+ });
35
40
  void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'source' } });
41
+ // @ts-expect-error Exchange options always name their mode.
42
+ void new ClientSession({ ...options, auth: credentialAuth, exchange: {} });
43
+ // @ts-expect-error Exchange options always name their mode.
44
+ void new ClientSession({ ...options, auth: credentialAuth, exchange: { ttlSeconds: 300 } });
45
+ // @ts-expect-error Source mode requests no Domain credential lifetime.
46
+ void new ClientSession({ ...options, exchange: { mode: 'source', ttlSeconds: 300 } });
36
47
  // @ts-expect-error Exchange accepts named modes, not a boolean switch.
37
48
  void new ClientSession({ ...options, exchange: true });
38
49
  // @ts-expect-error Unknown exchange modes are not admitted.
@@ -7,7 +7,6 @@ export interface AutoTransportOptions {
7
7
  readonly http: string;
8
8
  readonly websocket: string;
9
9
  };
10
- readonly codec?: 'json' | 'msgpack';
11
10
  readonly fetch?: Fetch;
12
11
  readonly websocket?: WebSocketConstructor;
13
12
  readonly headers?: Readonly<Record<string, string>>;
@@ -26,7 +26,6 @@ export class AutoTransport {
26
26
  this.onWarning = options.onWarning;
27
27
  this.http = new HttpTransport({
28
28
  url: options.endpoints.http,
29
- ...(options.codec === undefined ? {} : { codec: options.codec }),
30
29
  ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
31
30
  ...(options.headers === undefined ? {} : { headers: options.headers }),
32
31
  timeoutMs: this.timeoutMs,
@@ -39,7 +38,6 @@ export class AutoTransport {
39
38
  });
40
39
  this.websocket = new WebSocketTransport({
41
40
  url: options.endpoints.websocket,
42
- ...(options.codec === undefined ? {} : { codec: options.codec }),
43
41
  ...(options.websocket === undefined ? {} : { websocket: options.websocket }),
44
42
  timeoutMs: this.timeoutMs,
45
43
  ...(options.maximumRequestBytes === undefined
@@ -1,6 +1,6 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
3
- export declare function codecForResponse(contentType: string | null): Promise<codecs.Codec | undefined>;
3
+ export declare function codecForResponse(contentType: string | null): codecs.Codec | undefined;
4
4
  export declare function readBoundedBody(response: Response, maximumBytes: number, signal: AbortSignal): Promise<Uint8Array>;
5
5
  export declare function requireId(expected: protocol.RequestId, received: protocol.RequestId): void;
6
6
  export declare function decodeProtocol<T>(label: string, decode: () => T): T;
@@ -2,12 +2,8 @@ import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
3
3
  import { ProtocolError, ResponseError } from '../../errors/index.js';
4
4
  import { asError } from '../index.js';
5
- export async function codecForResponse(contentType) {
6
- const mediaType = normalizeMediaType(contentType);
7
- const supported = mediaType === codecs.CONTENT_TYPE_MSGPACK
8
- ? [codecs.json, await codecs.loadMsgpack()]
9
- : [codecs.json];
10
- return protocol.http.codecForContentType(contentType, supported);
5
+ export function codecForResponse(contentType) {
6
+ return protocol.http.codecForContentType(contentType, [codecs.json]);
11
7
  }
12
8
  export async function readBoundedBody(response, maximumBytes, signal) {
13
9
  const declared = response.headers.get('content-length');
@@ -3,7 +3,6 @@ import type { RequestOptions, Transport, TransportRequest, TransportResult } fro
3
3
  export type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
4
4
  export interface HttpTransportOptions {
5
5
  readonly url: string;
6
- readonly codec?: 'json' | 'msgpack';
7
6
  /** Inject this capability when the host does not provide global Fetch. */
8
7
  readonly fetch?: Fetch;
9
8
  readonly headers?: Readonly<Record<string, string>>;
@@ -16,7 +15,6 @@ export interface HttpTransportOptions {
16
15
  /** HTTP implementation of the universal invocation protocol. */
17
16
  export declare class HttpTransport implements Transport {
18
17
  readonly url: string;
19
- private readonly codecName;
20
18
  private readonly fetchImpl;
21
19
  private readonly headers;
22
20
  private readonly timeoutMs;
@@ -29,7 +27,11 @@ export declare class HttpTransport implements Transport {
29
27
  dispatchBinary(input: TransportRequest, body: Blob, options?: RequestOptions): Promise<TransportResult>;
30
28
  private dispatchWith;
31
29
  private fetchResponse;
30
+ /**
31
+ * Admit the reserved retirement control only as the exact 410, header and envelope triple; the
32
+ * header beside any other status, value or body is an invalid response, never an ordinary error.
33
+ */
34
+ private decodeDeploymentRetired;
32
35
  private decodeBinary;
33
36
  private nextRequestId;
34
- private requestCodec;
35
37
  }
@@ -1,6 +1,7 @@
1
1
  import { canonicalMediaType } from '@astrale-os/kernel-protocol/artifact';
2
2
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
+ import * as retirement from '@astrale-os/kernel-protocol/routes/deployment-retired';
4
5
  import { ProtocolError, ResponseError, TransportError } from '../../errors/index.js';
5
6
  import { invocationFailure } from '../failure.js';
6
7
  import { asError, controlledSignal, positiveInteger, requireLive, } from '../index.js';
@@ -13,7 +14,6 @@ const DEFAULT_MAXIMUM_RESPONSE_BYTES = 16 * 1024 * 1024;
13
14
  /** HTTP implementation of the universal invocation protocol. */
14
15
  export class HttpTransport {
15
16
  url;
16
- codecName;
17
17
  fetchImpl;
18
18
  headers;
19
19
  timeoutMs;
@@ -25,7 +25,6 @@ export class HttpTransport {
25
25
  throw new TypeError('HttpTransport options must be an object.');
26
26
  }
27
27
  this.url = normalizeUrl(options.url);
28
- this.codecName = admitCodecName(options.codec);
29
28
  this.fetchImpl = admitFetch(options.fetch);
30
29
  this.headers = admitHeaders(options.headers ?? {});
31
30
  this.timeoutMs = positiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 'timeoutMs');
@@ -47,7 +46,7 @@ export class HttpTransport {
47
46
  async dispatchWith(input, options, binaryBody) {
48
47
  const requestId = this.nextRequestId();
49
48
  const timeoutMs = positiveInteger(options.timeoutMs ?? this.timeoutMs, 'timeoutMs');
50
- const codec = await this.requestCodec();
49
+ const codec = codecs.json;
51
50
  const { trace: traceInput, ...invocationInput } = input;
52
51
  const trace = traceInput === undefined ? undefined : protocol.admitTrace(traceInput);
53
52
  const request = protocol.decodeRequest({
@@ -82,7 +81,15 @@ export class HttpTransport {
82
81
  const binaryDelivery = response.headers.get(protocol.http.BINARY_DELIVERY_FIELD);
83
82
  const hasStreamSignal = streamFraming !== null;
84
83
  const hasBinarySignal = binaryId !== null || binaryManifest !== null || binaryDelivery !== null;
85
- const responseCodec = await codecForResponse(response.headers.get('content-type'));
84
+ const responseCodec = codecForResponse(response.headers.get('content-type'));
85
+ if (response.headers.has(retirement.HTTP_DEPLOYMENT_RETIRED)) {
86
+ if (hasStreamSignal || hasBinarySignal) {
87
+ throw new ProtocolError('server returned a deployment-retired control with a stream or binary signal.');
88
+ }
89
+ const retired = await this.decodeDeploymentRetired(response, responseCodec, control);
90
+ requireLive(control);
91
+ return retired;
92
+ }
86
93
  if (!response.ok && responseCodec === undefined && !hasStreamSignal && !hasBinarySignal) {
87
94
  throw invocationFailure(`server returned HTTP ${response.status} without an invocation response.`, 'receive', { delivery: 'unknown' });
88
95
  }
@@ -182,6 +189,29 @@ export class HttpTransport {
182
189
  };
183
190
  return this.fetchImpl(this.url, init);
184
191
  }
192
+ /**
193
+ * Admit the reserved retirement control only as the exact 410, header and envelope triple; the
194
+ * header beside any other status, value or body is an invalid response, never an ordinary error.
195
+ */
196
+ async decodeDeploymentRetired(response, codec, control) {
197
+ if (codec === undefined) {
198
+ throw new ProtocolError('server returned a deployment-retired control without an envelope.');
199
+ }
200
+ requireNoStore(response);
201
+ const body = await readBoundedBody(response, this.maximumResponseBytes, control.signal);
202
+ const decoded = decodeProtocol('deployment-retired body', () => codec.decode(body));
203
+ const retired = decodeProtocol('deployment-retired control', () => retirement.decodeDeploymentRetired(response.status, response.headers, decoded));
204
+ const envelope = decodeProtocol('deployment-retired envelope', () => protocol.decodeResponse(decoded));
205
+ if (retired === undefined || !('error' in envelope)) {
206
+ throw new ProtocolError('server returned an invalid deployment-retired control.');
207
+ }
208
+ return Object.freeze({
209
+ kind: 'deployment-retired',
210
+ reason: retired.reason,
211
+ message: envelope.error.message,
212
+ invocation: envelope.invocation,
213
+ });
214
+ }
185
215
  async decodeBinary(response, requestId, binaryId, streaming, control) {
186
216
  if (binaryId !== String(requestId)) {
187
217
  throw new ProtocolError(`binary response requestId ${JSON.stringify(binaryId)} does not match request requestId ${requestId}.`);
@@ -197,14 +227,13 @@ export class HttpTransport {
197
227
  if (streaming) {
198
228
  if (response.body === null)
199
229
  throw new ProtocolError('streaming binary has no body.');
200
- // An intermediary may rewrite Content-Length (for example when it compresses the body), so the
201
- // protocol length wins; Content-Length only counts for a Server that predates the field.
202
- const declared = response.headers.get(protocol.http.BINARY_LENGTH_FIELD);
203
- const length = declared ?? response.headers.get('content-length');
230
+ // An intermediary may rewrite Content-Length (for example when it compresses the body), so
231
+ // only the protocol length counts.
232
+ const length = response.headers.get(protocol.http.BINARY_LENGTH_FIELD);
204
233
  const expectedBytes = length === null ? undefined : Number(length);
205
234
  if (length !== null &&
206
235
  (!/^(?:0|[1-9][0-9]*)$/u.test(length) || !Number.isSafeInteger(expectedBytes))) {
207
- throw new ProtocolError(`streaming binary has an invalid ${declared === null ? 'Content-Length' : protocol.http.BINARY_LENGTH_FIELD}.`);
236
+ throw new ProtocolError(`streaming binary has an invalid ${protocol.http.BINARY_LENGTH_FIELD}.`);
208
237
  }
209
238
  if (expectedBytes !== undefined && expectedBytes > this.maximumResponseBytes) {
210
239
  throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumResponseBytes}).`);
@@ -215,6 +244,8 @@ export class HttpTransport {
215
244
  invocation,
216
245
  value: {
217
246
  body: new HttpByteStream(response.body, this.maximumResponseBytes, control, expectedBytes),
247
+ // The byte stream enforces this exact count through its final read.
248
+ ...(expectedBytes === undefined ? {} : { length: expectedBytes }),
218
249
  mediaType,
219
250
  ...(response.status === 200 ? {} : { status: response.status }),
220
251
  ...(Object.keys(headers).length === 0 ? {} : { headers }),
@@ -249,9 +280,6 @@ export class HttpTransport {
249
280
  this.nextId = requestId === Number.MAX_SAFE_INTEGER ? 1 : requestId + 1;
250
281
  return requestId;
251
282
  }
252
- requestCodec() {
253
- return this.codecName === 'msgpack' ? codecs.loadMsgpack() : Promise.resolve(codecs.json);
254
- }
255
283
  }
256
284
  function redirectResult(response) {
257
285
  return Object.freeze({
@@ -340,13 +368,6 @@ function transportFailure(input, control, parent, timeoutMs, url, phase, deliver
340
368
  delivery,
341
369
  });
342
370
  }
343
- function admitCodecName(input) {
344
- if (input === undefined || input === 'json')
345
- return 'json';
346
- if (input === 'msgpack')
347
- return input;
348
- throw new TypeError('codec must be "json" or "msgpack".');
349
- }
350
371
  function admitFetch(input) {
351
372
  const implementation = input ?? globalThis.fetch;
352
373
  if (typeof implementation !== 'function') {
@@ -1,2 +1,2 @@
1
- export type { RequestOptions, MissControl, StaleControl, Transport, TransportRequest, TransportResult, } from './transport.js';
1
+ export type { RequestOptions, DeploymentRetired, MissControl, StaleControl, Transport, TransportRequest, TransportResult, } from './transport.js';
2
2
  export { asError, controlledSignal, positiveInteger, requireLive, type ControlledSignal, } from './deadline.js';
@@ -1,4 +1,4 @@
1
- import type { invocation as protocol } from '@astrale-os/kernel-protocol';
1
+ import type { invocation as protocol, routes } from '@astrale-os/kernel-protocol';
2
2
  import type { Result } from '../client/result.js';
3
3
  export interface RequestOptions {
4
4
  readonly signal?: AbortSignal;
@@ -25,7 +25,15 @@ export interface MissControl {
25
25
  readonly kind: 'miss';
26
26
  readonly invocation: protocol.InvocationId;
27
27
  }
28
- export type TransportResult = Result | StaleControl | MissControl;
28
+ /**
29
+ * A retired deployment's reserved 410 control: its typed `DEPLOYMENT_RETIRED` reason with the
30
+ * message and invocation of the envelope it carried, which Client raises unchanged.
31
+ */
32
+ export interface DeploymentRetired extends routes.DeploymentRetired {
33
+ readonly message: string;
34
+ readonly invocation: protocol.InvocationId;
35
+ }
36
+ export type TransportResult = Result | StaleControl | MissControl | DeploymentRetired;
29
37
  export interface Transport {
30
38
  /** Primary embedding-environment adapter for a complete invocation boundary. */
31
39
  dispatch(input: TransportRequest, options?: RequestOptions): Promise<TransportResult>;
@@ -3,7 +3,6 @@ import type { RequestOptions, Transport, TransportRequest, TransportResult } fro
3
3
  export type WebSocketConstructor = new (url: string | URL, protocols?: string | string[]) => WebSocket;
4
4
  export interface WebSocketTransportOptions {
5
5
  readonly url: string;
6
- readonly codec?: 'json' | 'msgpack';
7
6
  /** Inject this capability when the host does not provide a global WebSocket. */
8
7
  readonly websocket?: WebSocketConstructor;
9
8
  readonly timeoutMs?: number;
@@ -22,7 +21,6 @@ export interface WebSocketTransportOptions {
22
21
  export declare class WebSocketTransport implements Transport {
23
22
  readonly url: string;
24
23
  private readonly connectionUrl;
25
- private readonly codecName;
26
24
  private readonly WebSocketConstructor;
27
25
  private readonly timeoutMs;
28
26
  private readonly maximumRequestBytes;
@@ -42,5 +40,4 @@ export declare class WebSocketTransport implements Transport {
42
40
  private connectionClosed;
43
41
  private acquire;
44
42
  private nextRequestId;
45
- private codec;
46
43
  }