@astrale-os/kernel-client 0.6.0-beta.70 → 0.6.0-beta.72

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 (40) hide show
  1. package/dist/auth/auth.d.ts +17 -5
  2. package/dist/auth/auth.js +3 -4
  3. package/dist/auth/credential.d.ts +5 -1
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.js +0 -1
  6. package/dist/client/client.d.ts +4 -2
  7. package/dist/client/client.js +13 -2
  8. package/dist/client/credential.d.ts +4 -2
  9. package/dist/client/credential.js +7 -2
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/client/index.js +1 -1
  12. package/dist/client/request.d.ts +7 -1
  13. package/dist/client/request.js +31 -6
  14. package/dist/index.d.ts +1 -1
  15. package/dist/session/admission.d.ts +18 -0
  16. package/dist/session/admission.js +117 -0
  17. package/dist/session/bound.d.ts +12 -9
  18. package/dist/session/bound.js +24 -5
  19. package/dist/session/content.d.ts +30 -0
  20. package/dist/session/content.js +160 -0
  21. package/dist/session/context/operation.d.ts +5 -1
  22. package/dist/session/discovery/index.d.ts +1 -0
  23. package/dist/session/discovery/index.js +1 -0
  24. package/dist/session/index.d.ts +3 -2
  25. package/dist/session/reference.d.ts +7 -3
  26. package/dist/session/reference.js +1 -1
  27. package/dist/session/routing/dispatch.d.ts +10 -0
  28. package/dist/session/routing/dispatch.js +68 -23
  29. package/dist/session/routing/index.d.ts +1 -1
  30. package/dist/session/routing/index.js +1 -1
  31. package/dist/session/routing/request.d.ts +9 -0
  32. package/dist/session/routing/request.js +17 -0
  33. package/dist/session/session.d.ts +12 -3
  34. package/dist/session/session.js +58 -13
  35. package/dist/session/session.typecheck.js +46 -1
  36. package/dist/transport/http/byte-stream.d.ts +2 -1
  37. package/dist/transport/http/byte-stream.js +9 -1
  38. package/dist/transport/http/transport.d.ts +3 -0
  39. package/dist/transport/http/transport.js +42 -10
  40. package/package.json +6 -6
@@ -0,0 +1,160 @@
1
+ import { NodeId } from '@astrale-os/kernel-core/graph/node';
2
+ import { Path } from '@astrale-os/kernel-core/path';
3
+ import { K } from '@astrale-os/kernel-core/schema';
4
+ import { download } from '@astrale-os/kernel-core/schema/kernel/syscalls/references';
5
+ import { PropertyKey } from '@astrale-os/kernel-dsl/v1/addressing';
6
+ import * as artifact from '@astrale-os/kernel-protocol/artifact';
7
+ import { CODES } from '@astrale-os/kernel-protocol/errors';
8
+ import { ClientError, ProtocolError, ResponseError, TransportError } from '../errors/index.js';
9
+ import { admitControlOptions, runSessionOperation } from './context/index.js';
10
+ import { reference } from './reference.js';
11
+ import { requireResultKind } from './routing/index.js';
12
+ const UPLOAD = reference(K, K.functions.upload);
13
+ const DOWNLOAD = Path.project(download);
14
+ const CONTENT_TIMEOUT_MS = 30 * 60 * 1_000;
15
+ const MAXIMUM_ATTEMPTS = 3;
16
+ /** Transfer immutable content over the authenticated Session routing conversation. */
17
+ export function createContentApi(dependencies) {
18
+ async function upload(source, options) {
19
+ const body = admitBody(source);
20
+ const mediaType = admitMediaType(options?.mediaType);
21
+ const operation = dependencies.operation(contentControls(options));
22
+ let last;
23
+ for (let attempt = 0; attempt < MAXIMUM_ATTEMPTS; attempt++) {
24
+ try {
25
+ const result = await dependencies.invoke(UPLOAD, { mediaType }, { body }, operation);
26
+ return uploadResult(result);
27
+ }
28
+ catch (cause) {
29
+ last = cause;
30
+ if (!retryable(cause) || attempt + 1 === MAXIMUM_ATTEMPTS || operation.signal.aborted) {
31
+ throw cause;
32
+ }
33
+ await runSessionOperation(operation, (signal) => delay(200 * 2 ** attempt, signal));
34
+ }
35
+ }
36
+ throw last;
37
+ }
38
+ async function download(location, options) {
39
+ const operation = dependencies.operation(contentControls(options));
40
+ const result = await dependencies.dispatch(DOWNLOAD, admitLocation(location), operation);
41
+ return verifiedDownload(requireResultKind('binary', result).value);
42
+ }
43
+ return Object.freeze({ upload, download });
44
+ }
45
+ function admitLocation(input) {
46
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
47
+ throw new TypeError('Content location is invalid.');
48
+ }
49
+ return Object.freeze({ node: NodeId(input.node), property: PropertyKey(input.property) });
50
+ }
51
+ function admitBody(input) {
52
+ if (typeof Blob !== 'undefined' && input instanceof Blob)
53
+ return input;
54
+ if (input instanceof Uint8Array) {
55
+ // Blob snapshots an ArrayBuffer view; a SharedArrayBuffer view needs an owned copy.
56
+ const bytes = input.buffer instanceof ArrayBuffer
57
+ ? input
58
+ : new Uint8Array(input);
59
+ return new Blob([bytes]);
60
+ }
61
+ throw new TypeError('Content upload source must be a Blob, File, or Uint8Array.');
62
+ }
63
+ function admitMediaType(input) {
64
+ if (typeof input !== 'string')
65
+ throw new TypeError('Content upload mediaType is required.');
66
+ return artifact.identify(new Uint8Array(), input).mediaType;
67
+ }
68
+ function contentControls(input) {
69
+ const controls = admitControlOptions(input);
70
+ return Object.freeze({ ...controls, timeoutMs: controls.timeoutMs ?? CONTENT_TIMEOUT_MS });
71
+ }
72
+ function uploadResult(input) {
73
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
74
+ throw new ClientError('Content upload returned an invalid receipt.');
75
+ }
76
+ const value = input;
77
+ if (typeof value.receipt !== 'string' ||
78
+ value.receipt.length === 0 ||
79
+ typeof value.expiresAt !== 'string' ||
80
+ !Number.isFinite(Date.parse(value.expiresAt))) {
81
+ throw new ClientError('Content upload returned an invalid receipt.');
82
+ }
83
+ let ref;
84
+ try {
85
+ ref = artifact.accept(value.ref);
86
+ }
87
+ catch (cause) {
88
+ throw new ClientError('Content upload returned an invalid Ref.', { cause });
89
+ }
90
+ return Object.freeze({
91
+ receipt: value.receipt,
92
+ ref,
93
+ expiresAt: value.expiresAt,
94
+ });
95
+ }
96
+ function retryable(cause) {
97
+ return ((cause instanceof TransportError && cause.context.kind === 'invocation') ||
98
+ (cause instanceof ResponseError &&
99
+ (cause.reason?.code === 'BLOB_UNAVAILABLE' ||
100
+ cause.code === CODES.CANCELLED ||
101
+ cause.code === CODES.DEADLINE_EXCEEDED ||
102
+ cause.code === CODES.OUTCOME_UNKNOWN)));
103
+ }
104
+ function delay(ms, signal) {
105
+ return new Promise((resolve, reject) => {
106
+ if (signal.aborted)
107
+ return reject(signal.reason);
108
+ const timer = setTimeout(() => {
109
+ signal.removeEventListener('abort', abort);
110
+ resolve();
111
+ }, ms);
112
+ const abort = () => {
113
+ clearTimeout(timer);
114
+ reject(signal.reason);
115
+ };
116
+ signal.addEventListener('abort', abort, { once: true });
117
+ });
118
+ }
119
+ async function verifiedDownload(input) {
120
+ const raw = input.headers?.['x-astrale-blob-size'];
121
+ if (raw === undefined || !/^(?:0|[1-9][0-9]*)$/u.test(raw)) {
122
+ await cancelBinary(input);
123
+ throw new ProtocolError('Content download omitted its exact content size.');
124
+ }
125
+ const expected = Number(raw);
126
+ if (!Number.isSafeInteger(expected)) {
127
+ await cancelBinary(input);
128
+ throw new ProtocolError('Content download content size is invalid.');
129
+ }
130
+ if (input.body instanceof Uint8Array) {
131
+ if (input.body.byteLength !== expected) {
132
+ throw new ProtocolError('Content download ended before its exact content size.');
133
+ }
134
+ return input;
135
+ }
136
+ return Object.freeze({ ...input, body: exactContentBytes(input.body, expected) });
137
+ }
138
+ async function cancelBinary(input) {
139
+ if (input.body instanceof Uint8Array)
140
+ return;
141
+ try {
142
+ await input.body[Symbol.asyncIterator]().return?.();
143
+ }
144
+ catch {
145
+ // Preserve the admission error after the response stream has been closed.
146
+ }
147
+ }
148
+ async function* exactContentBytes(body, expected) {
149
+ let received = 0;
150
+ for await (const bytes of body) {
151
+ received += bytes.byteLength;
152
+ if (received > expected) {
153
+ throw new ProtocolError('Content download exceeded its exact content size.');
154
+ }
155
+ yield bytes;
156
+ }
157
+ if (received !== expected) {
158
+ throw new ProtocolError('Content download ended before its exact content size.');
159
+ }
160
+ }
@@ -13,7 +13,11 @@ export declare function createSessionOperation(lifecycle: AbortSignal, input: {
13
13
  readonly idempotencyKey?: RequestOptions['idempotencyKey'];
14
14
  } | undefined, defaultTimeoutMs: number): SessionOperation;
15
15
  export declare function remainingSessionTime(operation: SessionOperation): number;
16
- export declare function sessionTransportOptions(operation: SessionOperation, idempotencyKey?: RequestOptions['idempotencyKey']): RequestOptions;
16
+ export declare function sessionTransportOptions(operation: SessionOperation): {
17
+ readonly signal: AbortSignal;
18
+ readonly timeoutMs: number;
19
+ };
20
+ export declare function sessionTransportOptions(operation: SessionOperation, idempotencyKey: RequestOptions['idempotencyKey']): RequestOptions;
17
21
  /** Settle independently of a caller capability that ignores its supplied AbortSignal. */
18
22
  export declare function runSessionOperation<Result>(operation: SessionOperation, task: (signal: AbortSignal) => Result | PromiseLike<Result>): Promise<Result>;
19
23
  export declare function requireSessionOperation(operation: SessionOperation): void;
@@ -4,6 +4,7 @@ import type { Fetch } from '../../transport/http/index.js';
4
4
  import type { SessionOperation, SessionPolicy } from '../context/index.js';
5
5
  import type { SessionSnapshot } from './snapshot.js';
6
6
  export type { SessionSnapshot } from './snapshot.js';
7
+ export { boundedResponse, requireExactResponse } from './publication.js';
7
8
  export interface SessionDiscovery {
8
9
  readonly kernel: IssuerId;
9
10
  publication(operation: SessionOperation): Promise<publication.Publication>;
@@ -1,6 +1,7 @@
1
1
  import { BundleDiscovery } from './bundle.js';
2
2
  import { SessionDiscovery as SessionDiscoveryImplementation } from './discovery.js';
3
3
  import { PublicationDiscovery } from './publication.js';
4
+ export { boundedResponse, requireExactResponse } from './publication.js';
4
5
  export function createSessionDiscovery(options) {
5
6
  return new SessionDiscoveryImplementation(options.kernel, options.policy, new PublicationDiscovery({
6
7
  kernel: options.kernel,
@@ -1,10 +1,11 @@
1
1
  export type { SessionControlOptions, SessionPolicy } from './context/index.js';
2
+ export type { ContentApi, ContentLocation, ContentUploadOptions, ContentUploadResult, } from './content.js';
2
3
  export { bindSession, type BoundClientSession, type BoundSessionControls, type BoundSessionSource, } from './bound.js';
3
4
  export { createSessionCredentialProvider, type SessionCredential, type SessionCredentialProvider, type SessionCredentialProviderOptions, type SessionCredentialState, } from './credential/index.js';
4
5
  export type { SessionSnapshot } from './discovery/index.js';
5
- export type { Auth, PersistedRoute, SessionRouteArtifact, SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, } from './routing/index.js';
6
+ export type { Auth, PersistedRoute, SessionRouteArtifact, SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody, } from './routing/index.js';
6
7
  export { ClientSession, connect, type ClientSessionOptions, type ConnectOptions, } from './session.js';
7
- export type { CallableReference, InputOfReference, InstanceCallableReference, OutputOfReference, } from './reference.js';
8
+ export type { CallableReference, InputOfReference, InputModeOfReference, InstanceCallableReference, OutputOfReference, } from './reference.js';
8
9
  export { reference } from './reference.js';
9
10
  export type { ResolvedView, ViewCatalog } from './views/index.js';
10
11
  export type { DomainBinding } from '../graph/domain/index.js';
@@ -1,12 +1,13 @@
1
1
  import type { Path as PathValue, PathLike } from '@astrale-os/kernel-core/path';
2
- import type { CallableInputOf, CallableOutputOf, CallableResultOf, Domain, ResolvedFunction, ExecutableMethod } from '@astrale-os/kernel-dsl/v1';
2
+ import type { CallableInputOf, CallableInputModeOf, CallableOutputOf, CallableResultOf, Domain, ResolvedFunction, ExecutableMethod } from '@astrale-os/kernel-dsl/v1';
3
3
  import type { Revision } 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';
7
7
  declare const REFERENCE_INPUT: unique symbol;
8
+ declare const REFERENCE_INPUT_MODE: unique symbol;
8
9
  declare const REFERENCE_OUTPUT: unique symbol;
9
- export interface CallableReference<InputValue = Input, Output = unknown> {
10
+ export interface CallableReference<InputValue = Input, Output = unknown, InputMode extends 'value' | 'binary' = 'value' | 'binary'> {
10
11
  readonly origin: KeyType.Origin;
11
12
  /** @deprecated Informational only; dispatch pins `fingerprint` and revision pinning will be removed. */
12
13
  readonly revision: Revision;
@@ -16,6 +17,7 @@ export interface CallableReference<InputValue = Input, Output = unknown> {
16
17
  readonly target: PathValue;
17
18
  readonly outputMode: 'value' | 'stream' | 'binary';
18
19
  readonly [REFERENCE_INPUT]?: InputValue;
20
+ readonly [REFERENCE_INPUT_MODE]?: InputMode;
19
21
  readonly [REFERENCE_OUTPUT]?: Output;
20
22
  }
21
23
  export interface InstanceCallableReference<InputValue = Input, Output = unknown> {
@@ -28,13 +30,15 @@ export interface InstanceCallableReference<InputValue = Input, Output = unknown>
28
30
  }
29
31
  export type InputOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer InputValue, unknown> ? InputValue : never;
30
32
  export type OutputOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer Output> ? Output : never;
33
+ export type InputModeOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer _Output, infer Mode> ? Mode : never;
31
34
  type ReferenceInput<Callable> = unknown extends CallableInputOf<Callable> ? Input : CallableInputOf<Callable>;
32
35
  type ReferenceOutput<Callable> = unknown extends CallableOutputOf<Callable> ? unknown : CallableResultOf<Callable, Binary>;
33
36
  type ExecutableStaticMethod = ExecutableMethod<unknown, unknown, true>;
34
37
  type ExecutableInstanceMethod = ExecutableMethod<unknown, unknown, false>;
35
38
  /** Project one exact resolved Function or static Method into a typed Client reference. */
36
- export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
39
+ export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>, CallableInputModeOf<Callable>>;
37
40
  /** Project one exact resolved instance Method into a receiver-bound typed Client reference. */
38
41
  export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): InstanceCallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
39
42
  export declare function admitReference(input: CallableReference<unknown>): CallableReference<unknown>;
43
+ export declare function matchesCallable(target: PathValue, callable: KeyType.Callable): boolean;
40
44
  export {};
@@ -52,7 +52,7 @@ export function admitReference(input) {
52
52
  outputMode: input.outputMode,
53
53
  });
54
54
  }
55
- function matchesCallable(target, callable) {
55
+ export function matchesCallable(target, callable) {
56
56
  const coordinate = callableAddress(callable);
57
57
  const { anchor, steps } = target.ast;
58
58
  const last = steps.at(-1);
@@ -1,5 +1,6 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
2
  import type { Result } from '../../client/index.js';
3
+ import type { Fetch } from '../../transport/http/index.js';
3
4
  import type { SessionOperation, SessionPolicy } from '../context/index.js';
4
5
  import type { SessionAuth } from './auth.js';
5
6
  import type { RouteCache } from './cache.js';
@@ -16,15 +17,24 @@ export interface SessionDispatcherOptions {
16
17
  readonly policy: Readonly<SessionPolicy>;
17
18
  readonly envelopeTransport: SessionEnvelopeTransport;
18
19
  readonly requireOpen: () => void;
20
+ readonly streamingHttp: {
21
+ readonly fetch: Fetch;
22
+ readonly maximumResponseBytes: number;
23
+ readonly timeoutMs: number;
24
+ };
19
25
  }
20
26
  export declare class SessionDispatcher {
21
27
  private readonly state;
22
28
  constructor(state: SessionDispatcherOptions);
23
29
  dispatch(input: Parameters<typeof acceptCall>[0], options: SessionRequestOptions, operation: SessionOperation): Promise<Result>;
30
+ /** Reuse Session authority and route recovery while forcing HTTP for streaming calls. */
31
+ dispatchHttp(input: Parameters<typeof acceptCall>[0], options: SessionRequestOptions, operation: SessionOperation, body?: Blob): Promise<Result>;
32
+ private dispatchWith;
24
33
  private dispatchSource;
25
34
  private followSource;
26
35
  private dispatchDestinationWithRecovery;
27
36
  private dispatchDestination;
37
+ private streamingClientFor;
28
38
  }
29
39
  export declare function requireResultKind<Kind extends 'value' | 'stream' | 'binary'>(expected: Kind, result: Result): Extract<Result, {
30
40
  kind: Kind;
@@ -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, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
3
+ import { acceptCall, Client, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
4
4
  import { ClientError, ResponseError } from '../../errors/index.js';
5
5
  import { runSessionOperation, sessionTransportOptions, allowEndpoint } from '../context/index.js';
6
6
  import { resolveAuth } from './auth.js';
@@ -11,6 +11,13 @@ export class SessionDispatcher {
11
11
  this.state = state;
12
12
  }
13
13
  async dispatch(input, options, operation) {
14
+ return this.dispatchWith(input, options, operation);
15
+ }
16
+ /** Reuse Session authority and route recovery while forcing HTTP for streaming calls. */
17
+ async dispatchHttp(input, options, operation, body) {
18
+ return this.dispatchWith(input, options, operation, { body });
19
+ }
20
+ async dispatchWith(input, options, operation, streaming) {
14
21
  this.state.requireOpen();
15
22
  const call = acceptCall(input);
16
23
  // Authority is resolved before warm lookup so changed credentials or delegation cannot reuse a
@@ -27,7 +34,7 @@ export class SessionDispatcher {
27
34
  let learned = await this.state.cache.get(cacheKey, auth.credential);
28
35
  if (learned !== undefined) {
29
36
  try {
30
- requireCachedRoute(learned, source, call.target, this.state.envelopeTransport, this.state.websocketAvailable, this.state.policy);
37
+ requireCachedRoute(learned, source, call.target, streaming === undefined ? this.state.envelopeTransport : 'http', this.state.websocketAvailable, this.state.policy);
31
38
  }
32
39
  catch {
33
40
  this.state.cache.delete(cacheKey);
@@ -35,22 +42,31 @@ export class SessionDispatcher {
35
42
  }
36
43
  }
37
44
  if (learned !== undefined) {
38
- return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation);
45
+ return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation, undefined, streaming);
39
46
  }
40
- const result = await this.dispatchSource(dispatch, undefined, options, operation);
41
- return this.followSource(result, dispatch, options, operation);
47
+ const result = await this.dispatchSource(dispatch, undefined, options, operation, streaming);
48
+ return this.followSource(result, dispatch, options, operation, streaming);
42
49
  }
43
- async dispatchSource(state, routing, options, operation) {
50
+ async dispatchSource(state, routing, options, operation, streaming) {
44
51
  this.state.requireOpen();
45
52
  allowEndpoint(this.state.sourceEndpoint, this.state.policy);
46
- const client = this.state.pool.clientFor(Object.freeze({ transport: 'http', url: this.state.sourceEndpoint }));
47
- return runSessionOperation(operation, () => authenticated(client, state.auth.credential).dispatch(state.call, {
48
- ...sessionTransportOptions(operation, options.idempotencyKey),
49
- ...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
50
- ...(routing === undefined ? {} : { routing }),
51
- }));
53
+ const client = streaming === undefined
54
+ ? this.state.pool.clientFor(Object.freeze({ transport: 'http', url: this.state.sourceEndpoint }))
55
+ : this.streamingClientFor(this.state.sourceEndpoint);
56
+ return runSessionOperation(operation, () => streaming?.body === undefined
57
+ ? authenticated(client, state.auth.credential).dispatch(state.call, {
58
+ ...sessionTransportOptions(operation, options.idempotencyKey),
59
+ ...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
60
+ ...(routing === undefined ? {} : { routing }),
61
+ })
62
+ : authenticated(client, state.auth.credential).dispatch(state.call, {
63
+ ...sessionTransportOptions(operation),
64
+ ...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
65
+ ...(routing === undefined ? {} : { routing }),
66
+ body: streaming.body,
67
+ }));
52
68
  }
53
- async followSource(result, state, options, operation) {
69
+ async followSource(result, state, options, operation, streaming) {
54
70
  if (result.kind !== 'redirect')
55
71
  return result;
56
72
  const redirect = result.redirect;
@@ -59,11 +75,11 @@ export class SessionDispatcher {
59
75
  }
60
76
  requireTargetPublication(redirect.route, state.call.target);
61
77
  const learned = await this.state.cache.learn(state.cacheKey, redirect.route, redirect.credential, state.auth.credential);
62
- return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace);
78
+ return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace, streaming);
63
79
  }
64
- async dispatchDestinationWithRecovery(learned, state, options, operation, trace) {
80
+ async dispatchDestinationWithRecovery(learned, state, options, operation, trace, streaming) {
65
81
  try {
66
- return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace);
82
+ return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace, streaming);
67
83
  }
68
84
  catch (cause) {
69
85
  const stale = isAdmittedStaleRouteError(cause);
@@ -83,20 +99,31 @@ export class SessionDispatcher {
83
99
  stale: learned.route.via.publication.etag,
84
100
  })
85
101
  : undefined;
86
- const refreshed = await this.dispatchSource(state, routing, options, operation);
87
- return this.followSource(refreshed, state, options, operation);
102
+ const refreshed = await this.dispatchSource(state, routing, options, operation, streaming);
103
+ return this.followSource(refreshed, state, options, operation, streaming);
88
104
  }
89
105
  }
90
- async dispatchDestination(learned, call, options, operation, source, trace) {
106
+ async dispatchDestination(learned, call, options, operation, source, trace, streaming) {
91
107
  if (learned.route.via.issuer !== source) {
92
108
  throw new ClientError('Learned route issuer differs from the session-pinned source Kernel.');
93
109
  }
94
110
  requireTargetPublication(learned.route, call.target);
95
- const envelope = selectEnvelope(learned.route.endpoint, this.state.envelopeTransport, this.state.websocketAvailable, this.state.pool, this.state.policy);
111
+ const destination = streaming === undefined
112
+ ? undefined
113
+ : selectEnvelopeTarget(learned.route.endpoint, 'http', this.state.websocketAvailable, this.state.policy);
114
+ const client = streaming === undefined
115
+ ? selectEnvelope(learned.route.endpoint, this.state.envelopeTransport, this.state.websocketAvailable, this.state.pool, this.state.policy).client
116
+ : destination?.transport === 'http'
117
+ ? this.streamingClientFor(destination.url)
118
+ : invalidStreamingDestination();
96
119
  const requestOptions = {
97
120
  ...sessionTransportOptions(operation, options.idempotencyKey),
98
121
  routing: learned.route.via,
99
122
  };
123
+ const binaryRequestOptions = {
124
+ ...sessionTransportOptions(operation),
125
+ routing: learned.route.via,
126
+ };
100
127
  // The source has admitted this fingerprint against the exact Publication carried by Via.
101
128
  // Pin that revision at the destination so existing Workers can execute the same admitted
102
129
  // callable without understanding the newer fingerprint Target representation. The cache
@@ -111,14 +138,32 @@ export class SessionDispatcher {
111
138
  }),
112
139
  }
113
140
  : call;
114
- const result = await runSessionOperation(operation, () => trace === undefined
115
- ? authenticated(envelope.client, learned.credential).dispatch(destinationCall, requestOptions)
116
- : dispatchTrace(envelope.client, learned.credential, destinationCall, requestOptions, trace));
141
+ const result = await runSessionOperation(operation, () => streaming?.body !== undefined
142
+ ? trace === undefined
143
+ ? authenticated(client, learned.credential).dispatch(destinationCall, {
144
+ ...binaryRequestOptions,
145
+ body: streaming.body,
146
+ })
147
+ : dispatchTrace(client, learned.credential, destinationCall, { ...binaryRequestOptions, body: streaming.body }, trace)
148
+ : trace === undefined
149
+ ? authenticated(client, learned.credential).dispatch(destinationCall, requestOptions)
150
+ : dispatchTrace(client, learned.credential, destinationCall, requestOptions, trace));
117
151
  if (result.kind === 'redirect') {
118
152
  throw new ClientError('A destination redirect is invalid; only the source Kernel may redirect.');
119
153
  }
120
154
  return result;
121
155
  }
156
+ streamingClientFor(url) {
157
+ return new Client({
158
+ url,
159
+ fetch: this.state.streamingHttp.fetch,
160
+ timeoutMs: this.state.streamingHttp.timeoutMs,
161
+ maximumResponseBytes: this.state.streamingHttp.maximumResponseBytes,
162
+ });
163
+ }
164
+ }
165
+ function invalidStreamingDestination() {
166
+ throw new ClientError('Streaming route has no HTTP endpoint.');
122
167
  }
123
168
  function requireCachedRoute(learned, source, target, envelopeTransport, websocketAvailable, policy) {
124
169
  if (learned.route.via.issuer !== source) {
@@ -2,4 +2,4 @@ export { admitSessionAuth, resolveAuth, type Auth, type EffectiveAuth, type Sess
2
2
  export { RouteCache, type LearnedRoute, type PersistedRoute, type SessionRouteArtifact, type SessionRouteStore, } from './cache.js';
3
3
  export { requireResultKind, SessionDispatcher, type SessionDispatcherOptions } from './dispatch.js';
4
4
  export { type ClientPoolCapability, type EnvelopeSelection } from './envelope.js';
5
- export { admitEnvelopeTransport, admitRequestOptions, type SessionEnvelopeTransport, type SessionRequestOptions, } from './request.js';
5
+ export { admitEnvelopeTransport, admitInvocationOptions, admitRequestOptions, type SessionEnvelopeTransport, type SessionRequestOptions, type SessionRequestOptionsWithBody, } from './request.js';
@@ -2,4 +2,4 @@ export { admitSessionAuth, resolveAuth, } from './auth.js';
2
2
  export { RouteCache, } from './cache.js';
3
3
  export { requireResultKind, SessionDispatcher } from './dispatch.js';
4
4
  export {} from './envelope.js';
5
- export { admitEnvelopeTransport, admitRequestOptions, } from './request.js';
5
+ export { admitEnvelopeTransport, admitInvocationOptions, admitRequestOptions, } from './request.js';
@@ -3,6 +3,15 @@ import type { SessionControlOptions } from '../context/index.js';
3
3
  export type SessionEnvelopeTransport = 'auto' | 'http' | 'websocket';
4
4
  export interface SessionRequestOptions extends SessionControlOptions {
5
5
  readonly idempotencyKey?: invocation.IdempotencyKey;
6
+ readonly body?: never;
6
7
  }
8
+ export type SessionRequestOptionsWithBody = Omit<SessionRequestOptions, 'body' | 'idempotencyKey'> & {
9
+ readonly body: Blob;
10
+ readonly idempotencyKey?: never;
11
+ };
12
+ export declare function admitInvocationOptions(input: SessionRequestOptions | SessionRequestOptionsWithBody | undefined): {
13
+ readonly options: Readonly<SessionRequestOptions>;
14
+ readonly body?: Blob;
15
+ };
7
16
  export declare function admitRequestOptions(input?: SessionRequestOptions): Readonly<SessionRequestOptions>;
8
17
  export declare function admitEnvelopeTransport(input: SessionEnvelopeTransport): SessionEnvelopeTransport;
@@ -1,5 +1,22 @@
1
1
  import * as invocationProtocol from '@astrale-os/kernel-protocol/invocation';
2
2
  import { admitControlOptions } from '../context/index.js';
3
+ export function admitInvocationOptions(input) {
4
+ if (input === undefined)
5
+ return Object.freeze({ options: admitRequestOptions() });
6
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
7
+ throw new TypeError('Session request options are invalid.');
8
+ }
9
+ if (!Object.hasOwn(input, 'body')) {
10
+ return Object.freeze({ options: admitRequestOptions(input) });
11
+ }
12
+ const { body, ...options } = input;
13
+ if (!(body instanceof Blob))
14
+ throw new TypeError('Binary input must be a Blob or File.');
15
+ if (options.idempotencyKey !== undefined) {
16
+ throw new TypeError('Binary requests cannot carry an idempotency key.');
17
+ }
18
+ return Object.freeze({ options: admitRequestOptions(options), body });
19
+ }
3
20
  export function admitRequestOptions(input = {}) {
4
21
  if (input === null || typeof input !== 'object' || Array.isArray(input)) {
5
22
  throw new TypeError('Session request options are invalid.');
@@ -2,6 +2,7 @@ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
2
  import type { PathLike } from '@astrale-os/kernel-core/path';
3
3
  import type { Domain } from '@astrale-os/kernel-dsl/v1/domain';
4
4
  import type { publication } from '@astrale-os/kernel-protocol';
5
+ import type { AuthApi } from '../auth/index.js';
5
6
  import type { Call, Input, Result } from '../client/index.js';
6
7
  import type { DomainBinding, GraphApi } from '../graph/index.js';
7
8
  import type { ClientFactory, ClientPool as ClientPoolType } from '../pool/index.js';
@@ -9,10 +10,11 @@ import type { SchemaApi } from '../schema/index.js';
9
10
  import type { ClientWarning } from '../transport/auto/index.js';
10
11
  import type { Fetch } from '../transport/http/index.js';
11
12
  import type { WebSocketConstructor } from '../transport/websocket/index.js';
13
+ import type { ContentApi } from './content.js';
12
14
  import type { SessionControlOptions, SessionPolicy } from './context/index.js';
13
15
  import type { SessionSnapshot } from './discovery/index.js';
14
- import type { CallableReference, InputOfReference, OutputOfReference } from './reference.js';
15
- import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions } from './routing/index.js';
16
+ import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
17
+ import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
16
18
  import type { ViewCatalog } from './views/index.js';
17
19
  export interface ClientSessionOptions {
18
20
  /** Exact canonical source Kernel identity; its invocation and discovery endpoints are derived. */
@@ -42,6 +44,8 @@ export declare class ClientSession {
42
44
  readonly kernel: IssuerId;
43
45
  readonly schema: SchemaApi;
44
46
  readonly graph: GraphApi;
47
+ readonly auth: AuthApi;
48
+ readonly content: ContentApi;
45
49
  private readonly ownedPool?;
46
50
  private readonly discovery;
47
51
  private readonly dispatcher;
@@ -58,8 +62,13 @@ export declare class ClientSession {
58
62
  snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
59
63
  viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
60
64
  dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
65
+ dispatch(call: Call, options: SessionRequestOptionsWithBody): Promise<Result>;
61
66
  call(call: Call, options?: SessionRequestOptions): Promise<Input>;
62
- invoke<const Reference extends CallableReference<unknown>>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
67
+ call(call: Call, options: SessionRequestOptionsWithBody): Promise<Input>;
68
+ invoke<const Reference extends CallableReference<unknown>>(reference: InputModeOfReference<Reference> extends 'binary' ? never : Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
69
+ invoke<const Reference extends CallableReference<unknown, unknown, 'binary'>>(reference: Reference, input: InputOfReference<Reference>, options: SessionRequestOptionsWithBody): Promise<OutputOfReference<Reference>>;
70
+ private dispatchWith;
71
+ private invokeWith;
63
72
  stream(call: Call, options?: SessionRequestOptions): Promise<import("../client/result.js").Stream>;
64
73
  binary(call: Call, options?: SessionRequestOptions): Promise<import("@astrale-os/kernel-core/callable").BinaryOutput>;
65
74
  close(): void;