@astrale-os/kernel-client 0.6.0-beta.71 → 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.
@@ -13,7 +13,11 @@ export interface ImpersonateOptions extends MintOptions {
13
13
  export interface DelegateOptions {
14
14
  readonly audience: string;
15
15
  readonly ttlSeconds: number;
16
- readonly attenuation: UnresolvedIdentityExpr;
16
+ /**
17
+ * Sent only when present; the Kernel reads absence as self. A Kernel whose `Identity.delegate`
18
+ * still requires it rejects the omission.
19
+ */
20
+ readonly attenuation?: UnresolvedIdentityExpr;
17
21
  }
18
22
  export declare const IDENTITY_IMPERSONATE_PATH: NodePath;
19
23
  export declare const IDENTITY_MINT_PATH: NodePath;
@@ -1,14 +1,14 @@
1
1
  import type { invocation as protocol } from '@astrale-os/kernel-protocol';
2
2
  import type { Transport } from '../transport/index.js';
3
3
  import type { AuthenticatedClient, RemoteCredential } from './credential.js';
4
- import type { Call, ClientDispatchOptions } from './request.js';
4
+ import type { Call, ClientDispatchOptions, ClientDispatchOptionsWithBody } from './request.js';
5
5
  import type { BinaryResponse, Result, StreamResult, ValueResult } from './result.js';
6
6
  import { type HttpTransportOptions } from '../transport/http/index.js';
7
7
  export type ClientOptions = HttpTransportOptions | {
8
8
  readonly transport: Transport;
9
9
  };
10
10
  /** Package-private redirect continuation path; intentionally absent from the public Client facade. */
11
- export declare function dispatchTrace(client: Client, credential: RemoteCredential | undefined, call: Call, options: ClientDispatchOptions | undefined, trace: protocol.Trace): Promise<Result>;
11
+ export declare function dispatchTrace(client: Client, credential: RemoteCredential | undefined, call: Call, options: ClientDispatchOptions | ClientDispatchOptionsWithBody | undefined, trace: protocol.Trace): Promise<Result>;
12
12
  /** Pure client for the universal remote Function invocation boundary. */
13
13
  export declare class Client implements AuthenticatedClient {
14
14
  private readonly transport;
@@ -16,7 +16,9 @@ export declare class Client implements AuthenticatedClient {
16
16
  private closed;
17
17
  constructor(options: ClientOptions);
18
18
  dispatch(call: Call, options?: ClientDispatchOptions): Promise<Result>;
19
+ dispatch(call: Call, options: ClientDispatchOptionsWithBody): Promise<Result>;
19
20
  call(call: Call, options?: ClientDispatchOptions): Promise<ValueResult>;
21
+ call(call: Call, options: ClientDispatchOptionsWithBody): Promise<ValueResult>;
20
22
  stream(call: Call, options?: ClientDispatchOptions): Promise<StreamResult>;
21
23
  binary(call: Call, options?: ClientDispatchOptions): Promise<BinaryResponse>;
22
24
  as(credential: RemoteCredential): AuthenticatedClient;
@@ -30,7 +30,7 @@ export class Client {
30
30
  return this.dispatchWith(undefined, call, options);
31
31
  }
32
32
  call(call, options) {
33
- return requireValue(this.dispatch(call, options));
33
+ return requireValue(this.dispatchWith(undefined, call, options));
34
34
  }
35
35
  stream(call, options) {
36
36
  return requireStream(this.dispatch(call, options));
@@ -57,7 +57,18 @@ export class Client {
57
57
  ? undefined
58
58
  : await resolveCredential(credential, request.options.signal);
59
59
  requireRequestLive(request.options.signal);
60
- const result = await this.transport.dispatch(invocation(request, resolved, trace), request.options);
60
+ const envelope = invocation(request, resolved, trace);
61
+ let result;
62
+ if (request.body === undefined) {
63
+ result = await this.transport.dispatch(envelope, request.options);
64
+ }
65
+ else {
66
+ const transport = this.transport;
67
+ if (!(transport instanceof HttpTransport)) {
68
+ throw new TypeError('Binary dispatch requires HTTP transport.');
69
+ }
70
+ result = await transport.dispatchBinary(envelope, request.body, request.options);
71
+ }
61
72
  if (result.kind === 'stale')
62
73
  throw admittedStaleRouteError(result.current, result.invocation);
63
74
  if (result.kind === 'miss')
@@ -1,12 +1,14 @@
1
- import type { Call, ClientDispatchOptions } from './request.js';
1
+ import type { Call, ClientDispatchOptions, ClientDispatchOptionsWithBody } from './request.js';
2
2
  import type { BinaryResponse, Result, StreamResult, ValueResult } from './result.js';
3
3
  export type RemoteCredential = string | (() => string | Promise<string>);
4
4
  export interface AuthenticatedClient {
5
5
  dispatch(call: Call, options?: ClientDispatchOptions): Promise<Result>;
6
+ dispatch(call: Call, options: ClientDispatchOptionsWithBody): Promise<Result>;
6
7
  call(call: Call, options?: ClientDispatchOptions): Promise<ValueResult>;
8
+ call(call: Call, options: ClientDispatchOptionsWithBody): Promise<ValueResult>;
7
9
  stream(call: Call, options?: ClientDispatchOptions): Promise<StreamResult>;
8
10
  binary(call: Call, options?: ClientDispatchOptions): Promise<BinaryResponse>;
9
11
  }
10
- export type CredentialDispatch = (credential: RemoteCredential, call: Call, options?: ClientDispatchOptions) => Promise<Result>;
12
+ export type CredentialDispatch = (credential: RemoteCredential, call: Call, options?: ClientDispatchOptions | ClientDispatchOptionsWithBody) => Promise<Result>;
11
13
  export declare function authenticated(dispatchWith: CredentialDispatch, credential: RemoteCredential): AuthenticatedClient;
12
14
  export declare function resolveCredential(input: RemoteCredential, signal?: AbortSignal): Promise<string>;
@@ -1,10 +1,15 @@
1
1
  import { ClientError } from '../errors/index.js';
2
2
  import { requireBinary, requireStream, requireValue } from './result.js';
3
3
  export function authenticated(dispatchWith, credential) {
4
- const dispatch = (call, options) => dispatchWith(credential, call, options);
4
+ function dispatch(call, options) {
5
+ return dispatchWith(credential, call, options);
6
+ }
7
+ function call(request, options) {
8
+ return requireValue(dispatchWith(credential, request, options));
9
+ }
5
10
  return Object.freeze({
6
11
  dispatch,
7
- call: (call, options) => requireValue(dispatch(call, options)),
12
+ call,
8
13
  stream: (call, options) => requireStream(dispatch(call, options)),
9
14
  binary: (call, options) => requireBinary(dispatch(call, options)),
10
15
  });
@@ -1,6 +1,6 @@
1
1
  export { Client, dispatchTrace, type ClientOptions } from './client.js';
2
2
  export { acceptCall } from './admission.js';
3
3
  export type { AuthenticatedClient, RemoteCredential } from './credential.js';
4
- export { call, type Call, type ClientDispatchOptions } from './request.js';
4
+ export { call, type Call, type ClientDispatchOptions, type ClientDispatchOptionsWithBody, } from './request.js';
5
5
  export type { Binary, BinaryResponse, Input, RedirectResult, Result, Stream, StreamResult, ValueResult, } from './result.js';
6
6
  export { isAdmittedMissError, isAdmittedStaleRouteError } from './routing-control.js';
@@ -1,4 +1,4 @@
1
1
  export { Client, dispatchTrace } from './client.js';
2
2
  export { acceptCall } from './admission.js';
3
- export { call } from './request.js';
3
+ export { call, } from './request.js';
4
4
  export { isAdmittedMissError, isAdmittedStaleRouteError } from './routing-control.js';
@@ -9,19 +9,25 @@ export interface Call {
9
9
  readonly input: Input;
10
10
  }
11
11
  export interface ClientDispatchOptions extends RequestOptions {
12
+ readonly body?: never;
12
13
  /** Untrusted delivery context; it changes neither Call meaning nor authority. */
13
14
  readonly delegate?: protocol.Delegate;
14
15
  readonly routing?: protocol.Routing;
15
16
  /** Package-owned local-target or remote-revision precondition for a bound invocation. */
16
17
  readonly schema?: protocol.SchemaTarget;
17
18
  }
19
+ export type ClientDispatchOptionsWithBody = Omit<ClientDispatchOptions, 'body' | 'idempotencyKey'> & {
20
+ readonly body: Blob;
21
+ readonly idempotencyKey?: never;
22
+ };
18
23
  export interface AdmittedRequest {
19
24
  readonly call: protocol.Call;
20
25
  readonly delegate?: protocol.Delegate;
21
26
  readonly routing?: protocol.Routing;
22
27
  readonly schema?: protocol.SchemaTarget;
23
28
  readonly options: RequestOptions;
29
+ readonly body?: Blob;
24
30
  }
25
31
  export declare function call(target: PathLike | Target, input: Input): Call;
26
- export declare function admitRequest(input: Call, options: ClientDispatchOptions | undefined, lifecycle: AbortSignal): AdmittedRequest;
32
+ export declare function admitRequest(input: Call, options: ClientDispatchOptions | ClientDispatchOptionsWithBody | undefined, lifecycle: AbortSignal): AdmittedRequest;
27
33
  export declare function invocation(request: AdmittedRequest, credential?: string, trace?: protocol.Trace): TransportRequest;
@@ -5,7 +5,8 @@ export function call(target, input) {
5
5
  }
6
6
  export function admitRequest(input, options, lifecycle) {
7
7
  const call = acceptCall(input);
8
- const admittedOptions = lifecycleOptions(admitRequestOptions(options), lifecycle);
8
+ const admitted = admitRequestOptions(options);
9
+ const admittedOptions = lifecycleOptions(admitted.options, lifecycle);
9
10
  const delegate = admitDelegate(options?.delegate);
10
11
  const routing = admitRouting(options?.routing);
11
12
  const schema = admitSchemaTarget(options?.schema);
@@ -14,6 +15,7 @@ export function admitRequest(input, options, lifecycle) {
14
15
  ...(delegate === undefined ? {} : { delegate }),
15
16
  ...(routing === undefined ? {} : { routing }),
16
17
  ...(schema === undefined ? {} : { schema }),
18
+ ...(admitted.body === undefined ? {} : { body: admitted.body }),
17
19
  options: admittedOptions,
18
20
  });
19
21
  }
@@ -41,11 +43,19 @@ export function invocation(request, credential, trace) {
41
43
  }
42
44
  function admitRequestOptions(input) {
43
45
  if (input === undefined)
44
- return undefined;
46
+ return {};
45
47
  if (input === null || typeof input !== 'object' || Array.isArray(input)) {
46
48
  throw new TypeError('Client request options must be an object.');
47
49
  }
48
- if (Reflect.ownKeys(input).some((field) => !['signal', 'timeoutMs', 'idempotencyKey', 'delegate', 'routing', 'schema'].includes(String(field)))) {
50
+ if (Reflect.ownKeys(input).some((field) => ![
51
+ 'signal',
52
+ 'timeoutMs',
53
+ 'idempotencyKey',
54
+ 'delegate',
55
+ 'routing',
56
+ 'schema',
57
+ 'body',
58
+ ].includes(String(field)))) {
49
59
  throw new TypeError('Client request options contain unsupported fields.');
50
60
  }
51
61
  if (input.timeoutMs !== undefined &&
@@ -54,13 +64,28 @@ function admitRequestOptions(input) {
54
64
  input.timeoutMs > 2_147_483_647)) {
55
65
  throw new TypeError('Client request timeoutMs must be a positive safe platform integer.');
56
66
  }
67
+ const hasBody = Object.hasOwn(input, 'body');
68
+ let body;
69
+ if (hasBody) {
70
+ const candidate = input.body;
71
+ if (!(candidate instanceof Blob)) {
72
+ throw new TypeError('Binary input must be a Blob or File.');
73
+ }
74
+ body = candidate;
75
+ }
76
+ if (body !== undefined && input.idempotencyKey !== undefined) {
77
+ throw new TypeError('Binary requests cannot carry an idempotency key.');
78
+ }
57
79
  const idempotencyKey = input.idempotencyKey === undefined
58
80
  ? undefined
59
81
  : invocationProtocol.acceptIdempotencyKey(input.idempotencyKey, 'request.idempotencyKey');
60
82
  return Object.freeze({
61
- ...(input.signal === undefined ? {} : { signal: input.signal }),
62
- ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
63
- ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
83
+ options: Object.freeze({
84
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
85
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
86
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
87
+ }),
88
+ ...(body === undefined ? {} : { body }),
64
89
  });
65
90
  }
66
91
  function admitDelegate(input) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Target, targetPath } from '@astrale-os/kernel-core/callable';
2
- export { Client, call, type AuthenticatedClient, type Call, type ClientDispatchOptions, type ClientOptions, type Binary, type BinaryResponse, type Input, type RemoteCredential, type Result, type Stream, type StreamResult, type ValueResult, } from './client/index.js';
2
+ export { Client, call, type AuthenticatedClient, type Call, type ClientDispatchOptions, type ClientDispatchOptionsWithBody, type ClientOptions, type Binary, type BinaryResponse, type Input, type RemoteCredential, type Result, type Stream, type StreamResult, type ValueResult, } from './client/index.js';
3
3
  export * from './errors/index.js';
4
4
  export { type RequestOptions, type MissControl, type StaleControl, type Transport, type TransportRequest, type TransportResult, } from './transport/index.js';
5
5
  export { HttpTransport, type Fetch, type HttpTransportOptions } from './transport/http/index.js';
@@ -1,21 +1,22 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth';
2
2
  import type { AuthApi } from '../auth/index.js';
3
3
  import type { SchemaApi } from '../schema/index.js';
4
+ import type { ContentApi } from './content.js';
4
5
  import type { SessionControlOptions } from './context/index.js';
5
- import type { CallableReference, InputOfReference, OutputOfReference } from './reference.js';
6
6
  import type { SessionRequestOptions } from './routing/index.js';
7
7
  import type { ClientSession } from './session.js';
8
8
  import type { ViewCatalog } from './views/index.js';
9
- import { type Binary, type Call, type Input, type Result, type Stream } from '../client/index.js';
9
+ import { type Binary, type Call, type Stream } from '../client/index.js';
10
10
  import { type GraphApi } from '../graph/index.js';
11
11
  /** Exact live Session surface captured by one credential-bound Kernel view. */
12
12
  export interface BoundSessionSource {
13
13
  readonly kernel: IssuerId;
14
14
  readonly schema: SchemaApi;
15
15
  readonly auth: Pick<AuthApi, 'admit'>;
16
- dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
17
- call(call: Call, options?: SessionRequestOptions): Promise<Input>;
18
- invoke<const Reference extends CallableReference<unknown>>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
16
+ readonly content?: ContentApi;
17
+ dispatch: ClientSession['dispatch'];
18
+ call: ClientSession['call'];
19
+ invoke: ClientSession['invoke'];
19
20
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
20
21
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
21
22
  viewsFor(target: Parameters<ClientSession['viewsFor']>[0], options?: SessionControlOptions): Promise<ViewCatalog>;
@@ -35,9 +36,10 @@ export interface BoundClientSession extends GraphApi {
35
36
  readonly kernel: IssuerId;
36
37
  readonly auth: AuthApi;
37
38
  readonly schema: SchemaApi;
38
- dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
39
- call(call: Call, options?: SessionRequestOptions): Promise<Input>;
40
- invoke<const Reference extends CallableReference<unknown>>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
39
+ readonly content: ContentApi;
40
+ dispatch: ClientSession['dispatch'];
41
+ call: ClientSession['call'];
42
+ invoke: ClientSession['invoke'];
41
43
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
42
44
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
43
45
  viewsFor: BoundSessionSource['viewsFor'];
@@ -12,14 +12,22 @@ export function bindSession(source, controls) {
12
12
  const invokeSource = capture(source, 'invoke');
13
13
  const streamSource = capture(source, 'stream');
14
14
  const binarySource = capture(source, 'binary');
15
+ const contentSource = source.content === undefined ? unavailableContent : source.content;
16
+ if (contentSource === null || typeof contentSource !== 'object')
17
+ invalid();
18
+ const uploadSource = capture(contentSource, 'upload');
19
+ const downloadSource = capture(contentSource, 'download');
15
20
  const viewsForSource = capture(source, 'viewsFor');
16
21
  const schemaSource = captureSchema(source.schema);
17
- const dispatch = (input, options) => dispatchSource(input, requestOptions(options, admittedControls));
22
+ const dispatch = ((input, options) => dispatchSource(input, requestOptions(options, admittedControls)));
18
23
  const admit = (input, options) => admitSource(input, mergeOptions(options, admittedControls));
19
- const call = (input, options) => callSource(input, requestOptions(options, admittedControls));
20
- const invoke = (reference, input, options) => invokeSource(reference, input, requestOptions(options, admittedControls));
24
+ const call = ((input, options) => callSource(input, requestOptions(options, admittedControls)));
25
+ const invoke = ((reference, input, options) => invokeSource(reference, input, requestOptions(options, admittedControls)));
21
26
  const stream = (input, options) => streamSource(input, requestOptions(options, admittedControls));
22
27
  const binary = (input, options) => binarySource(input, requestOptions(options, admittedControls));
28
+ const upload = (input, options) => uploadSource(input, mergeOptions(options, admittedControls));
29
+ const download = (location, options) => downloadSource(location, controlOptions(options, admittedControls));
30
+ const content = Object.freeze({ upload, download });
23
31
  const viewsFor = (target, options) => viewsForSource(target, controlOptions(options, admittedControls));
24
32
  const install = (request, options) => schemaSource.install(request, controlOptions(options, admittedControls));
25
33
  const uninstall = (request, options) => schemaSource.uninstall(request, controlOptions(options, admittedControls));
@@ -39,9 +47,18 @@ export function bindSession(source, controls) {
39
47
  invoke,
40
48
  stream,
41
49
  binary,
50
+ content,
42
51
  viewsFor,
43
52
  });
44
53
  }
54
+ const unavailableContent = Object.freeze({
55
+ upload: (() => {
56
+ throw new TypeError('Content transfer is unavailable from this bound Session source.');
57
+ }),
58
+ download: () => {
59
+ throw new TypeError('Content transfer is unavailable from this bound Session source.');
60
+ },
61
+ });
45
62
  function admitControls(input) {
46
63
  if (input === undefined)
47
64
  return undefined;
@@ -0,0 +1,30 @@
1
+ import type { schema } from '@astrale-os/kernel-dsl/v1';
2
+ import type { Ref } from '@astrale-os/kernel-protocol/artifact';
3
+ import { Path } from '@astrale-os/kernel-core/path';
4
+ import type { Binary, Input, Result } from '../client/index.js';
5
+ import type { SessionControlOptions, SessionOperation } from './context/index.js';
6
+ import type { CallableReference } from './reference.js';
7
+ import type { SessionRequestOptionsWithBody } from './routing/index.js';
8
+ export interface ContentLocation {
9
+ readonly node: string;
10
+ readonly property: string;
11
+ }
12
+ export interface ContentUploadOptions extends SessionControlOptions {
13
+ readonly mediaType: string;
14
+ }
15
+ export interface ContentUploadResult {
16
+ readonly receipt: schema.values.BlobReceipt;
17
+ readonly ref: Ref;
18
+ readonly expiresAt: string;
19
+ }
20
+ export interface ContentApi {
21
+ upload(source: Blob | Uint8Array, options: ContentUploadOptions): Promise<ContentUploadResult>;
22
+ download(location: ContentLocation, options?: SessionControlOptions): Promise<Binary>;
23
+ }
24
+ export interface ContentSessionDependencies {
25
+ readonly operation: (options: SessionControlOptions) => SessionOperation;
26
+ readonly invoke: (reference: CallableReference<unknown, unknown, 'binary'>, input: unknown, options: SessionRequestOptionsWithBody, operation: SessionOperation) => Promise<unknown>;
27
+ readonly dispatch: (target: Path, input: Input, operation: SessionOperation, body?: Blob) => Promise<Result>;
28
+ }
29
+ /** Transfer immutable content over the authenticated Session routing conversation. */
30
+ export declare function createContentApi(dependencies: ContentSessionDependencies): ContentApi;
@@ -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;
@@ -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,12 +30,13 @@ 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>;
@@ -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;