@astrale-os/kernel-client 0.6.0-beta.71 → 0.6.0-beta.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credential.d.ts +6 -1
- package/dist/auth/credential.js +2 -2
- package/dist/client/client.d.ts +4 -2
- package/dist/client/client.js +13 -2
- package/dist/client/credential.d.ts +4 -2
- package/dist/client/credential.js +7 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/request.d.ts +7 -1
- package/dist/client/request.js +31 -6
- package/dist/index.d.ts +1 -1
- package/dist/schema/introspection.js +4 -0
- package/dist/session/admission.d.ts +3 -15
- package/dist/session/admission.js +16 -74
- package/dist/session/bound.d.ts +10 -8
- package/dist/session/bound.js +20 -3
- package/dist/session/content.d.ts +30 -0
- package/dist/session/content.js +160 -0
- package/dist/session/context/operation.d.ts +5 -1
- package/dist/session/credential/index.d.ts +1 -1
- package/dist/session/credential/index.js +1 -1
- package/dist/session/credential/provider.d.ts +5 -0
- package/dist/session/credential/provider.js +8 -1
- package/dist/session/exchange/domain.d.ts +27 -0
- package/dist/session/exchange/domain.js +63 -0
- package/dist/session/exchange/exchange.d.ts +54 -0
- package/dist/session/exchange/exchange.js +58 -0
- package/dist/session/exchange/index.d.ts +5 -0
- package/dist/session/exchange/index.js +5 -0
- package/dist/session/exchange/issuer.d.ts +18 -0
- package/dist/session/exchange/issuer.js +89 -0
- package/dist/session/exchange/limits.d.ts +10 -0
- package/dist/session/exchange/limits.js +10 -0
- package/dist/session/exchange/options.d.ts +29 -0
- package/dist/session/exchange/options.js +57 -0
- package/dist/session/exchange/presentation.d.ts +18 -0
- package/dist/session/exchange/presentation.js +73 -0
- package/dist/session/exchange/source.d.ts +34 -0
- package/dist/session/exchange/source.js +78 -0
- package/dist/session/execution.d.ts +65 -0
- package/dist/session/execution.js +35 -0
- package/dist/session/index.d.ts +5 -2
- package/dist/session/index.js +1 -0
- package/dist/session/reference.d.ts +13 -4
- package/dist/session/routing/auth.d.ts +10 -0
- package/dist/session/routing/auth.js +8 -1
- package/dist/session/routing/dispatch.d.ts +12 -1
- package/dist/session/routing/dispatch.js +73 -25
- package/dist/session/routing/index.d.ts +2 -2
- package/dist/session/routing/index.js +1 -1
- package/dist/session/routing/request.d.ts +9 -0
- package/dist/session/routing/request.js +17 -0
- package/dist/session/session.d.ts +26 -3
- package/dist/session/session.js +59 -14
- package/dist/session/session.typecheck.js +71 -2
- package/dist/transport/http/byte-stream.d.ts +2 -1
- package/dist/transport/http/byte-stream.js +9 -1
- package/dist/transport/http/transport.d.ts +3 -0
- package/dist/transport/http/transport.js +42 -11
- package/package.json +5 -5
|
@@ -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.');
|
|
@@ -10,10 +10,13 @@ import type { SchemaApi } from '../schema/index.js';
|
|
|
10
10
|
import type { ClientWarning } from '../transport/auto/index.js';
|
|
11
11
|
import type { Fetch } from '../transport/http/index.js';
|
|
12
12
|
import type { WebSocketConstructor } from '../transport/websocket/index.js';
|
|
13
|
+
import type { ContentApi } from './content.js';
|
|
13
14
|
import type { SessionControlOptions, SessionPolicy } from './context/index.js';
|
|
15
|
+
import type { SessionCredential } from './credential/index.js';
|
|
14
16
|
import type { SessionSnapshot } from './discovery/index.js';
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
17
|
+
import type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
18
|
+
import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
|
|
19
|
+
import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
|
|
17
20
|
import type { ViewCatalog } from './views/index.js';
|
|
18
21
|
export interface ClientSessionOptions {
|
|
19
22
|
/** Exact canonical source Kernel identity; its invocation and discovery endpoints are derived. */
|
|
@@ -23,6 +26,12 @@ export interface ClientSessionOptions {
|
|
|
23
26
|
readonly pool?: Pick<ClientPoolType, 'clientFor'>;
|
|
24
27
|
readonly websocket?: WebSocketConstructor;
|
|
25
28
|
readonly auth?: SessionAuth;
|
|
29
|
+
/**
|
|
30
|
+
* Present each Domain callable with a credential its declaring Domain exchanges for the `auth`
|
|
31
|
+
* caller, so the Domain is the principal and the caller is `auth`. Kernel and local callables keep
|
|
32
|
+
* the `auth` credential.
|
|
33
|
+
*/
|
|
34
|
+
readonly exchange?: SessionExchangeOptions;
|
|
26
35
|
/** Optional caller-owned persistence for bounded confidential route artifacts. */
|
|
27
36
|
readonly routeStore?: SessionRouteStore;
|
|
28
37
|
readonly policy: SessionPolicy;
|
|
@@ -44,24 +53,38 @@ export declare class ClientSession {
|
|
|
44
53
|
readonly schema: SchemaApi;
|
|
45
54
|
readonly graph: GraphApi;
|
|
46
55
|
readonly auth: AuthApi;
|
|
56
|
+
readonly content: ContentApi;
|
|
47
57
|
private readonly ownedPool?;
|
|
48
58
|
private readonly discovery;
|
|
49
59
|
private readonly dispatcher;
|
|
50
60
|
private readonly lifecycle;
|
|
51
61
|
private readonly timeoutMs;
|
|
52
62
|
private readonly routes;
|
|
63
|
+
private readonly exchangeDependencies;
|
|
64
|
+
private readonly callExchange?;
|
|
53
65
|
private readonly admitTargetViews;
|
|
54
66
|
private closed;
|
|
55
67
|
constructor(options: ClientSessionOptions);
|
|
56
68
|
/** Bind one admitted DSL Domain to authenticated graph projection and Method dispatch. */
|
|
57
69
|
bind<const DomainValue extends Domain>(domain: DomainValue): DomainBinding<DomainValue>;
|
|
70
|
+
/**
|
|
71
|
+
* Delegate this Session's caller to one Domain issuer and exchange that delegation for the
|
|
72
|
+
* Domain's credential to this Kernel. Nothing is cached: hold the result, for example through
|
|
73
|
+
* `createSessionCredentialProvider`, for as long as it covers the calls it carries.
|
|
74
|
+
*/
|
|
75
|
+
exchange(domain: string, options?: SessionExchangeRequest): Promise<SessionCredential>;
|
|
58
76
|
/** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
|
|
59
77
|
publication(options?: SessionControlOptions): Promise<publication.Publication>;
|
|
60
78
|
snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
|
|
61
79
|
viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
|
|
62
80
|
dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
|
|
81
|
+
dispatch(call: Call, options: SessionRequestOptionsWithBody): Promise<Result>;
|
|
63
82
|
call(call: Call, options?: SessionRequestOptions): Promise<Input>;
|
|
64
|
-
|
|
83
|
+
call(call: Call, options: SessionRequestOptionsWithBody): Promise<Input>;
|
|
84
|
+
invoke<const Reference extends CallableReference<unknown>>(reference: InputModeOfReference<Reference> extends 'binary' ? never : Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
|
|
85
|
+
invoke<const Reference extends CallableReference<unknown, unknown, 'binary'>>(reference: Reference, input: InputOfReference<Reference>, options: SessionRequestOptionsWithBody): Promise<OutputOfReference<Reference>>;
|
|
86
|
+
private dispatchWith;
|
|
87
|
+
private invokeWith;
|
|
65
88
|
stream(call: Call, options?: SessionRequestOptions): Promise<import("../client/result.js").Stream>;
|
|
66
89
|
binary(call: Call, options?: SessionRequestOptions): Promise<import("@astrale-os/kernel-core/callable").BinaryOutput>;
|
|
67
90
|
close(): void;
|
package/dist/session/session.js
CHANGED
|
@@ -11,10 +11,12 @@ import { AutoTransport } from '../transport/auto/index.js';
|
|
|
11
11
|
import { HttpTransport } from '../transport/http/index.js';
|
|
12
12
|
import { WebSocketTransport } from '../transport/websocket/index.js';
|
|
13
13
|
import { admitFunction } from './admission.js';
|
|
14
|
+
import { createContentApi } from './content.js';
|
|
14
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
16
|
import { createSessionDiscovery } from './discovery/index.js';
|
|
17
|
+
import { acquireDomainCredential, admitExchangeOptions, admitExchangeRequest, createSessionExchange, } from './exchange/index.js';
|
|
16
18
|
import { admitReference } from './reference.js';
|
|
17
|
-
import { admitEnvelopeTransport,
|
|
19
|
+
import { admitEnvelopeTransport, admitInvocationOptions, admitSessionAuth, requireResultKind, RouteCache, SessionDispatcher, } from './routing/index.js';
|
|
18
20
|
import { resolveViewsFor, targetViewCatalog } from './views/index.js';
|
|
19
21
|
const DEFAULT_SESSION_POLICY = Object.freeze({ maximumRouteAgeMs: 60_000 });
|
|
20
22
|
/** Caller-owned discovery and redirect-following conversation with one exact source Kernel. */
|
|
@@ -23,12 +25,15 @@ export class ClientSession {
|
|
|
23
25
|
schema;
|
|
24
26
|
graph;
|
|
25
27
|
auth;
|
|
28
|
+
content;
|
|
26
29
|
ownedPool;
|
|
27
30
|
discovery;
|
|
28
31
|
dispatcher;
|
|
29
32
|
lifecycle = new AbortController();
|
|
30
33
|
timeoutMs;
|
|
31
34
|
routes;
|
|
35
|
+
exchangeDependencies;
|
|
36
|
+
callExchange;
|
|
32
37
|
admitTargetViews;
|
|
33
38
|
closed = false;
|
|
34
39
|
constructor(options) {
|
|
@@ -84,28 +89,45 @@ export class ClientSession {
|
|
|
84
89
|
});
|
|
85
90
|
this.routes = new RouteCache(policy.maximumRouteAgeMs, undefined, options.routeStore);
|
|
86
91
|
const auth = admitSessionAuth(options.auth);
|
|
87
|
-
const
|
|
92
|
+
const exchange = admitExchangeOptions(options.exchange, auth);
|
|
93
|
+
this.exchangeDependencies = Object.freeze({
|
|
88
94
|
kernel: this.kernel,
|
|
89
|
-
auth,
|
|
90
95
|
source: () => {
|
|
91
96
|
allowEndpoint(sourceEndpoint, policy);
|
|
92
97
|
return pool.clientFor({ transport: 'http', url: sourceEndpoint });
|
|
93
98
|
},
|
|
99
|
+
...(auth === undefined ? {} : { auth }),
|
|
94
100
|
fetch: fetchImpl,
|
|
95
101
|
policy,
|
|
96
102
|
limits: { maximumPublicationBytes, maximumBundleBytes },
|
|
97
103
|
maximumResponseBytes,
|
|
98
|
-
|
|
104
|
+
operation: (signal) => this.operation(signal === undefined ? undefined : { signal }),
|
|
105
|
+
});
|
|
106
|
+
this.callExchange =
|
|
107
|
+
exchange === undefined || auth === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: createSessionExchange({ ...this.exchangeDependencies, auth }, exchange);
|
|
99
110
|
this.dispatcher = new SessionDispatcher({
|
|
100
111
|
kernel: this.kernel,
|
|
101
112
|
sourceEndpoint,
|
|
102
113
|
pool,
|
|
103
114
|
websocketAvailable,
|
|
104
115
|
auth,
|
|
116
|
+
...(this.callExchange === undefined ? {} : { exchange: this.callExchange }),
|
|
105
117
|
cache: this.routes,
|
|
106
118
|
policy,
|
|
107
119
|
envelopeTransport: admitEnvelopeTransport(options.envelopeTransport ?? 'http'),
|
|
108
120
|
requireOpen: () => this.requireOpen(),
|
|
121
|
+
streamingHttp: {
|
|
122
|
+
fetch: fetchImpl,
|
|
123
|
+
maximumResponseBytes: 100 * 1024 * 1024,
|
|
124
|
+
timeoutMs: 30 * 60 * 1_000,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
this.content = createContentApi({
|
|
128
|
+
operation: (options) => this.operation(options),
|
|
129
|
+
invoke: (reference, input, options, operation) => this.invokeWith(reference, input, options, operation),
|
|
130
|
+
dispatch: (target, input, operation, body) => this.dispatcher.dispatchHttp({ target, input }, {}, operation, body),
|
|
109
131
|
});
|
|
110
132
|
this.schema = createSchema((request, options) => this.dispatch(request, options), {
|
|
111
133
|
maximumPublicationBytes,
|
|
@@ -115,13 +137,22 @@ export class ClientSession {
|
|
|
115
137
|
this.graph = createGraph((call, options) => this.call(call, options));
|
|
116
138
|
this.auth = createAuth((target, input, options) => this.call({ target, input }, options), (call, options) => {
|
|
117
139
|
const operation = this.operation(admitControlOptions(options));
|
|
118
|
-
return admitFunction(
|
|
140
|
+
return admitFunction(this.exchangeDependencies, acceptCall(call), options, operation);
|
|
119
141
|
});
|
|
120
142
|
}
|
|
121
143
|
/** Bind one admitted DSL Domain to authenticated graph projection and Method dispatch. */
|
|
122
144
|
bind(domain) {
|
|
123
145
|
return createDomainBinding(domain, this.graph, (node, method, input) => this.invokeDomainMethod(domain, node, method, input));
|
|
124
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Delegate this Session's caller to one Domain issuer and exchange that delegation for the
|
|
149
|
+
* Domain's credential to this Kernel. Nothing is cached: hold the result, for example through
|
|
150
|
+
* `createSessionCredentialProvider`, for as long as it covers the calls it carries.
|
|
151
|
+
*/
|
|
152
|
+
async exchange(domain, options) {
|
|
153
|
+
const request = admitExchangeRequest(domain, options);
|
|
154
|
+
return acquireDomainCredential(this.exchangeDependencies, request, this.operation(request.control));
|
|
155
|
+
}
|
|
125
156
|
/** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
|
|
126
157
|
publication(options) {
|
|
127
158
|
return this.discovery.publication(this.operation(admitControlOptions(options)));
|
|
@@ -134,23 +165,32 @@ export class ClientSession {
|
|
|
134
165
|
return resolveViewsFor(async (call, operation) => requireResultKind('value', await this.dispatcher.dispatch(call, {}, operation)).value, this.admitTargetViews, target, this.operation(admitControlOptions(options)));
|
|
135
166
|
}
|
|
136
167
|
dispatch(call, options) {
|
|
137
|
-
|
|
138
|
-
return this.dispatcher.dispatch(call, admitted, this.operation(admitted));
|
|
168
|
+
return this.dispatchWith(call, options);
|
|
139
169
|
}
|
|
140
170
|
async call(call, options) {
|
|
141
|
-
const result = await this.
|
|
171
|
+
const result = await this.dispatchWith(call, options);
|
|
142
172
|
return requireResultKind('value', result).value;
|
|
143
173
|
}
|
|
144
174
|
async invoke(reference, input, options) {
|
|
175
|
+
return this.invokeWith(reference, input, options);
|
|
176
|
+
}
|
|
177
|
+
dispatchWith(call, options, requireHttp = false) {
|
|
178
|
+
const admitted = admitInvocationOptions(options);
|
|
179
|
+
const operation = this.operation(admitted.options);
|
|
180
|
+
return admitted.body === undefined && !requireHttp
|
|
181
|
+
? this.dispatcher.dispatch(call, admitted.options, operation)
|
|
182
|
+
: this.dispatcher.dispatchHttp(call, admitted.options, operation, admitted.body);
|
|
183
|
+
}
|
|
184
|
+
async invokeWith(reference, input, options, sharedOperation) {
|
|
145
185
|
const admittedReference = admitReference(reference);
|
|
146
|
-
const
|
|
186
|
+
const admitted = admitInvocationOptions(options);
|
|
147
187
|
const { anchor, steps } = admittedReference.target.ast;
|
|
148
188
|
const last = steps.at(-1);
|
|
149
189
|
const instance = last?.kind === 'method' && last.dispatch === 'instance'
|
|
150
190
|
? Path.fromAST({ anchor, steps: steps.slice(0, -1) }).raw
|
|
151
191
|
: undefined;
|
|
152
192
|
// A generated reference pins the callable meaning; a hand-written one still pins the revision.
|
|
153
|
-
const
|
|
193
|
+
const call = {
|
|
154
194
|
target: Object.freeze({
|
|
155
195
|
callable: admittedReference.callable,
|
|
156
196
|
...(admittedReference.fingerprint === undefined
|
|
@@ -159,20 +199,24 @@ export class ClientSession {
|
|
|
159
199
|
...(instance === undefined ? {} : { instance }),
|
|
160
200
|
}),
|
|
161
201
|
input,
|
|
162
|
-
}
|
|
202
|
+
};
|
|
203
|
+
const operation = sharedOperation ?? this.operation(admitted.options);
|
|
204
|
+
const result = await (admitted.body === undefined && admittedReference.outputMode !== 'binary'
|
|
205
|
+
? this.dispatcher.dispatch(call, admitted.options, operation)
|
|
206
|
+
: this.dispatcher.dispatchHttp(call, admitted.options, operation, admitted.body));
|
|
163
207
|
const narrowed = requireResultKind(admittedReference.outputMode, result);
|
|
164
|
-
return
|
|
208
|
+
return narrowed.kind === 'value'
|
|
165
209
|
? narrowed.value
|
|
166
210
|
: narrowed.kind === 'stream'
|
|
167
211
|
? narrowed.stream
|
|
168
|
-
: narrowed.value
|
|
212
|
+
: narrowed.value;
|
|
169
213
|
}
|
|
170
214
|
async stream(call, options) {
|
|
171
215
|
const result = await this.dispatch(call, options);
|
|
172
216
|
return requireResultKind('stream', result).stream;
|
|
173
217
|
}
|
|
174
218
|
async binary(call, options) {
|
|
175
|
-
const result = await this.
|
|
219
|
+
const result = await this.dispatchWith(call, options, true);
|
|
176
220
|
return requireResultKind('binary', result).value;
|
|
177
221
|
}
|
|
178
222
|
close() {
|
|
@@ -183,6 +227,7 @@ export class ClientSession {
|
|
|
183
227
|
this.ownedPool?.close();
|
|
184
228
|
this.routes.clear();
|
|
185
229
|
this.discovery.clear();
|
|
230
|
+
this.callExchange?.clear();
|
|
186
231
|
}
|
|
187
232
|
requireOpen() {
|
|
188
233
|
if (this.closed)
|
|
@@ -2,7 +2,9 @@ import { NodeId } from '@astrale-os/kernel-core/graph/node';
|
|
|
2
2
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
3
3
|
import { K, KernelSchema } from '@astrale-os/kernel-core/schema';
|
|
4
4
|
import { defineSchema, func, method, nodeClass, schema, valueSchema, } from '@astrale-os/kernel-dsl/v1';
|
|
5
|
-
import {
|
|
5
|
+
import { isBinaryInput } from '@astrale-os/kernel-dsl/v1/language';
|
|
6
|
+
import { call } from '../client/index.js';
|
|
7
|
+
import { ClientSession, createSessionCredentialProvider, execution, reference } from './index.js';
|
|
6
8
|
void new ClientSession(options);
|
|
7
9
|
void auth;
|
|
8
10
|
void control;
|
|
@@ -40,7 +42,10 @@ const renameInput = valueSchema()({
|
|
|
40
42
|
required: ['name'],
|
|
41
43
|
additionalProperties: false,
|
|
42
44
|
});
|
|
43
|
-
const
|
|
45
|
+
const registerInput = K.functions.register.input;
|
|
46
|
+
if (isBinaryInput(registerInput))
|
|
47
|
+
throw new TypeError('Register input must be a Value.');
|
|
48
|
+
const registrationInput = valueSchema()(registerInput);
|
|
44
49
|
const booleanOutput = valueSchema()({ type: 'boolean' });
|
|
45
50
|
const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck', {
|
|
46
51
|
dependencies: { kernel: KernelSchema },
|
|
@@ -68,9 +73,42 @@ const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck'
|
|
|
68
73
|
const registerReference = reference(typedDomain, typedDomain.functions.registerIdentity);
|
|
69
74
|
const staticRegisterReference = reference(typedDomain, typedDomain.classes.Person.static.methods.registerIdentity);
|
|
70
75
|
const renameReference = reference(typedDomain, typedDomain.classes.Person.methods.rename)(Path.id(NodeId('person')));
|
|
76
|
+
const blobReceipt = session.content
|
|
77
|
+
.upload(blobBytes, { mediaType: 'image/png' })
|
|
78
|
+
.then(({ receipt }) => receipt);
|
|
79
|
+
const boundBlobReceipt = boundSession.content
|
|
80
|
+
.upload(blobBytes, { mediaType: 'image/png' })
|
|
81
|
+
.then(({ receipt }) => receipt);
|
|
82
|
+
void blobReceipt;
|
|
83
|
+
void boundBlobReceipt;
|
|
71
84
|
void session.invoke(registerReference, registration);
|
|
72
85
|
void boundSession.invoke(staticRegisterReference, registration);
|
|
73
86
|
void session.invoke(renameReference, renameRequest);
|
|
87
|
+
const binaryUploadReference = reference(K, K.functions.upload);
|
|
88
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' }, { body: new Blob() });
|
|
89
|
+
void boundSession.invoke(binaryUploadReference, { mediaType: 'image/png' }, { body: new Blob() });
|
|
90
|
+
const rawBinaryCall = call(Path.parse('/:example.test:function.write'), { mediaType: 'image/png' });
|
|
91
|
+
void client.dispatch(rawBinaryCall, { body: new Blob() });
|
|
92
|
+
void client.as('credential').call(rawBinaryCall, { body: new Blob() });
|
|
93
|
+
// @ts-expect-error Binary Functions require a byte source.
|
|
94
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' });
|
|
95
|
+
// @ts-expect-error Value Functions reject binary bodies.
|
|
96
|
+
void session.invoke(registerReference, registration, { body: new Blob() });
|
|
97
|
+
// @ts-expect-error Bound binary Functions require a byte source.
|
|
98
|
+
void boundSession.invoke(binaryUploadReference, { mediaType: 'image/png' });
|
|
99
|
+
// @ts-expect-error Bound value Functions reject binary bodies.
|
|
100
|
+
void boundSession.invoke(registerReference, registration, { body: new Blob() });
|
|
101
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' },
|
|
102
|
+
// @ts-expect-error Binary invocations cannot carry an idempotency key.
|
|
103
|
+
{ body: new Blob(), idempotencyKey: 'retry' });
|
|
104
|
+
// @ts-expect-error Raw Session binary calls cannot carry an idempotency key.
|
|
105
|
+
void session.dispatch(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
106
|
+
// @ts-expect-error Raw Client binary calls cannot carry an idempotency key.
|
|
107
|
+
void client.dispatch(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
108
|
+
// @ts-expect-error Authenticated Client binary calls cannot carry an idempotency key.
|
|
109
|
+
void client.as('credential').call(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
110
|
+
void session.dispatch({ target: Path.parse('/:example.test:function.write'), input: {} }, { body: new Blob() });
|
|
111
|
+
void session.call({ target: Path.parse('/:example.test:function.write'), input: {} }, { body: new Blob() });
|
|
74
112
|
const registerInputIsRequest = true;
|
|
75
113
|
const renameInputIsInterface = true;
|
|
76
114
|
const renameOutputIsBoolean = true;
|
|
@@ -98,6 +136,37 @@ const abstractDomain = schema.resolve(defineSchema('client.reference.typecheck',
|
|
|
98
136
|
AbstractItem: nodeClass({ abstract: true, methods: { abstractMethod } }),
|
|
99
137
|
},
|
|
100
138
|
}));
|
|
139
|
+
const reminderOutput = valueSchema()({
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: { reminder: { type: 'string' } },
|
|
142
|
+
required: ['reminder'],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
});
|
|
145
|
+
const durableDomain = schema.resolve(defineSchema('client.durable.typecheck', {
|
|
146
|
+
classes: {
|
|
147
|
+
Invoice: nodeClass({
|
|
148
|
+
methods: {
|
|
149
|
+
remind: method({
|
|
150
|
+
auth: 'authenticated',
|
|
151
|
+
input: renameInput,
|
|
152
|
+
output: reminderOutput,
|
|
153
|
+
execution: { durable: true },
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
const remindReference = reference(durableDomain, durableDomain.classes.Invoice.methods.remind)(Path.id(NodeId('invoice-1')));
|
|
160
|
+
const durableReturnsExecution = true;
|
|
161
|
+
const outcomeIsTyped = true;
|
|
162
|
+
const statusIsTyped = true;
|
|
163
|
+
void session.invoke(execution.result(run), { waitMs: 1_000 });
|
|
164
|
+
void session.invoke(execution.signal(run), { name: 'approval', value: { by: 'billing' } });
|
|
165
|
+
// @ts-expect-error Force is the only cancellation option.
|
|
166
|
+
void session.invoke(execution.cancel(run), { waitMs: 1_000 });
|
|
167
|
+
void durableReturnsExecution;
|
|
168
|
+
void outcomeIsTyped;
|
|
169
|
+
void statusIsTyped;
|
|
101
170
|
// @ts-expect-error Abstract Methods are declarations and cannot become Client references.
|
|
102
171
|
reference(abstractDomain, abstractDomain.classes.AbstractItem.methods.abstractMethod);
|
|
103
172
|
// @ts-expect-error Exact identity is derived internally, never supplied as a Session option.
|
|
@@ -3,12 +3,13 @@ import type { ControlledSignal } from '../index.js';
|
|
|
3
3
|
export declare class HttpByteStream implements AsyncIterable<Uint8Array> {
|
|
4
4
|
private readonly maximumBytes;
|
|
5
5
|
private readonly control;
|
|
6
|
+
private readonly expectedBytes?;
|
|
6
7
|
private readonly reader;
|
|
7
8
|
private claimed;
|
|
8
9
|
private terminal;
|
|
9
10
|
private bytes;
|
|
10
11
|
private interruption;
|
|
11
|
-
constructor(body: ReadableStream<Uint8Array>, maximumBytes: number, control: ControlledSignal);
|
|
12
|
+
constructor(body: ReadableStream<Uint8Array>, maximumBytes: number, control: ControlledSignal, expectedBytes?: number | undefined);
|
|
12
13
|
[Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
|
|
13
14
|
cancel(reason?: unknown): Promise<void>;
|
|
14
15
|
private next;
|
|
@@ -4,14 +4,16 @@ import { asError } from '../index.js';
|
|
|
4
4
|
export class HttpByteStream {
|
|
5
5
|
maximumBytes;
|
|
6
6
|
control;
|
|
7
|
+
expectedBytes;
|
|
7
8
|
reader;
|
|
8
9
|
claimed = false;
|
|
9
10
|
terminal = false;
|
|
10
11
|
bytes = 0;
|
|
11
12
|
interruption;
|
|
12
|
-
constructor(body, maximumBytes, control) {
|
|
13
|
+
constructor(body, maximumBytes, control, expectedBytes) {
|
|
13
14
|
this.maximumBytes = maximumBytes;
|
|
14
15
|
this.control = control;
|
|
16
|
+
this.expectedBytes = expectedBytes;
|
|
15
17
|
this.reader = body.getReader();
|
|
16
18
|
const abort = () => void this.interrupt(control.signal.reason);
|
|
17
19
|
if (control.signal.aborted)
|
|
@@ -53,11 +55,17 @@ export class HttpByteStream {
|
|
|
53
55
|
if (this.interruption !== undefined)
|
|
54
56
|
throw this.interruption;
|
|
55
57
|
if (next.done) {
|
|
58
|
+
if (this.expectedBytes !== undefined && this.bytes !== this.expectedBytes) {
|
|
59
|
+
throw new ProtocolError(`streaming binary ended after ${this.bytes} bytes; expected ${this.expectedBytes}.`);
|
|
60
|
+
}
|
|
56
61
|
this.terminal = true;
|
|
57
62
|
this.control.close();
|
|
58
63
|
return { done: true, value: undefined };
|
|
59
64
|
}
|
|
60
65
|
this.bytes += next.value.byteLength;
|
|
66
|
+
if (this.expectedBytes !== undefined && this.bytes > this.expectedBytes) {
|
|
67
|
+
throw new ProtocolError(`streaming binary exceeded declared Content-Length (${this.expectedBytes}).`);
|
|
68
|
+
}
|
|
61
69
|
if (this.bytes > this.maximumBytes) {
|
|
62
70
|
throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumBytes}).`);
|
|
63
71
|
}
|
|
@@ -25,6 +25,9 @@ export declare class HttpTransport implements Transport {
|
|
|
25
25
|
private nextId;
|
|
26
26
|
constructor(options: HttpTransportOptions);
|
|
27
27
|
dispatch(input: TransportRequest, options?: RequestOptions): Promise<TransportResult>;
|
|
28
|
+
/** HTTP-only binary invocation; each call may replay a re-readable Blob. */
|
|
29
|
+
dispatchBinary(input: TransportRequest, body: Blob, options?: RequestOptions): Promise<TransportResult>;
|
|
30
|
+
private dispatchWith;
|
|
28
31
|
private fetchResponse;
|
|
29
32
|
private decodeBinary;
|
|
30
33
|
private nextRequestId;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { canonicalMediaType } from '@astrale-os/kernel-protocol/artifact';
|
|
1
2
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
3
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
4
|
import { ProtocolError, ResponseError, TransportError } from '../../errors/index.js';
|
|
4
5
|
import { invocationFailure } from '../failure.js';
|
|
5
6
|
import { asError, controlledSignal, positiveInteger, requireLive, } from '../index.js';
|
|
6
7
|
import { HttpByteStream } from './byte-stream.js';
|
|
7
|
-
import { binaryHeaders, codecForResponse, decodeProtocol,
|
|
8
|
+
import { binaryHeaders, codecForResponse, decodeProtocol, readBoundedBody, requireId, } from './response.js';
|
|
8
9
|
import { HttpStream } from './stream.js';
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
10
11
|
const DEFAULT_MAXIMUM_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
@@ -32,6 +33,18 @@ export class HttpTransport {
|
|
|
32
33
|
this.maximumResponseBytes = positiveInteger(options.maximumResponseBytes ?? DEFAULT_MAXIMUM_RESPONSE_BYTES, 'maximumResponseBytes');
|
|
33
34
|
}
|
|
34
35
|
async dispatch(input, options = {}) {
|
|
36
|
+
return this.dispatchWith(input, options);
|
|
37
|
+
}
|
|
38
|
+
/** HTTP-only binary invocation; each call may replay a re-readable Blob. */
|
|
39
|
+
async dispatchBinary(input, body, options = {}) {
|
|
40
|
+
if (!(body instanceof Blob))
|
|
41
|
+
throw new TypeError('Binary request body must be a Blob.');
|
|
42
|
+
if (options.idempotencyKey !== undefined) {
|
|
43
|
+
throw new TypeError('Binary requests cannot carry an idempotency key.');
|
|
44
|
+
}
|
|
45
|
+
return this.dispatchWith(input, options, body);
|
|
46
|
+
}
|
|
47
|
+
async dispatchWith(input, options, binaryBody) {
|
|
35
48
|
const requestId = this.nextRequestId();
|
|
36
49
|
const timeoutMs = positiveInteger(options.timeoutMs ?? this.timeoutMs, 'timeoutMs');
|
|
37
50
|
const codec = await this.requestCodec();
|
|
@@ -43,19 +56,21 @@ export class HttpTransport {
|
|
|
43
56
|
...invocationInput,
|
|
44
57
|
...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
|
|
45
58
|
});
|
|
46
|
-
const encoded =
|
|
59
|
+
const encoded = binaryBody === undefined
|
|
60
|
+
? codec.encode(request)
|
|
61
|
+
: protocol.http.encodeBinaryRequestEnvelope(request);
|
|
47
62
|
const control = controlledSignal(options.signal, timeoutMs);
|
|
48
63
|
let handedOff = false;
|
|
49
64
|
let phase = 'connect';
|
|
50
65
|
let delivery = 'not-sent';
|
|
51
66
|
try {
|
|
52
67
|
requireLive(control);
|
|
53
|
-
if (encodedByteLength(encoded) > this.maximumRequestBytes) {
|
|
68
|
+
if (binaryBody === undefined && encodedByteLength(encoded) > this.maximumRequestBytes) {
|
|
54
69
|
throw invocationFailure(`request exceeds maximumRequestBytes (${this.maximumRequestBytes}).`, 'send');
|
|
55
70
|
}
|
|
56
71
|
phase = 'send';
|
|
57
72
|
delivery = 'unknown';
|
|
58
|
-
const response = await this.fetchResponse(codec, encoded, trace, control);
|
|
73
|
+
const response = await this.fetchResponse(codec, encoded, trace, control, binaryBody);
|
|
59
74
|
phase = 'receive';
|
|
60
75
|
requireLive(control);
|
|
61
76
|
if (response.status >= 300 && response.status < 400) {
|
|
@@ -143,20 +158,26 @@ export class HttpTransport {
|
|
|
143
158
|
control.close();
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
|
-
async fetchResponse(codec, encoded, trace, control) {
|
|
161
|
+
async fetchResponse(codec, encoded, trace, control, binaryBody) {
|
|
147
162
|
const init = {
|
|
148
163
|
method: 'POST',
|
|
149
164
|
redirect: 'manual',
|
|
150
165
|
credentials: 'omit',
|
|
151
|
-
cache: 'no-store',
|
|
152
166
|
referrerPolicy: 'no-referrer',
|
|
167
|
+
// Fetch owns Content-Length; the protocol carries Blob.size across Fetch runtimes.
|
|
153
168
|
headers: {
|
|
154
169
|
...this.headers,
|
|
155
170
|
...protocol.http.encodeTraceHeaders(trace),
|
|
156
|
-
'content-type': codec.contentType,
|
|
171
|
+
'content-type': binaryBody === undefined ? codec.contentType : protocol.http.BINARY_REQUEST_CONTENT_TYPE,
|
|
157
172
|
accept: codec.contentType,
|
|
173
|
+
...(binaryBody === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: {
|
|
176
|
+
[protocol.http.REQUEST_ENVELOPE_FIELD]: encoded,
|
|
177
|
+
[protocol.http.BINARY_LENGTH_FIELD]: String(binaryBody.size),
|
|
178
|
+
}),
|
|
158
179
|
},
|
|
159
|
-
body: toBody(encoded),
|
|
180
|
+
body: binaryBody ?? toBody(encoded),
|
|
160
181
|
signal: control.signal,
|
|
161
182
|
};
|
|
162
183
|
return this.fetchImpl(this.url, init);
|
|
@@ -165,22 +186,32 @@ export class HttpTransport {
|
|
|
165
186
|
if (binaryId !== String(requestId)) {
|
|
166
187
|
throw new ProtocolError(`binary response requestId ${JSON.stringify(binaryId)} does not match request requestId ${requestId}.`);
|
|
167
188
|
}
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
189
|
+
const contentType = response.headers.get('content-type');
|
|
190
|
+
if (contentType === null) {
|
|
170
191
|
throw new ProtocolError('binary response has no media type.');
|
|
171
192
|
}
|
|
193
|
+
const mediaType = decodeProtocol('binary media type', () => canonicalMediaType(contentType));
|
|
172
194
|
const headerNames = decodeProtocol('binary application-header manifest', () => protocol.http.decodeBinaryHeaderNames(response.headers.get(protocol.http.BINARY_HEADERS_FIELD)));
|
|
173
195
|
const headers = binaryHeaders(response.headers, headerNames);
|
|
174
196
|
const invocation = decodeProtocol('binary InvocationId field', () => protocol.http.decodeInvocationId(response.headers.get(protocol.http.INVOCATION_FIELD)));
|
|
175
197
|
if (streaming) {
|
|
176
198
|
if (response.body === null)
|
|
177
199
|
throw new ProtocolError('streaming binary has no body.');
|
|
200
|
+
const length = response.headers.get('content-length');
|
|
201
|
+
const expectedBytes = length === null ? undefined : Number(length);
|
|
202
|
+
if (length !== null &&
|
|
203
|
+
(!/^(?:0|[1-9][0-9]*)$/u.test(length) || !Number.isSafeInteger(expectedBytes))) {
|
|
204
|
+
throw new ProtocolError('streaming binary has an invalid Content-Length.');
|
|
205
|
+
}
|
|
206
|
+
if (expectedBytes !== undefined && expectedBytes > this.maximumResponseBytes) {
|
|
207
|
+
throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumResponseBytes}).`);
|
|
208
|
+
}
|
|
178
209
|
requireLive(control);
|
|
179
210
|
return {
|
|
180
211
|
kind: 'binary',
|
|
181
212
|
invocation,
|
|
182
213
|
value: {
|
|
183
|
-
body: new HttpByteStream(response.body, this.maximumResponseBytes, control),
|
|
214
|
+
body: new HttpByteStream(response.body, this.maximumResponseBytes, control, expectedBytes),
|
|
184
215
|
mediaType,
|
|
185
216
|
...(response.status === 200 ? {} : { status: response.status }),
|
|
186
217
|
...(Object.keys(headers).length === 0 ? {} : { headers }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-client",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.74",
|
|
4
4
|
"description": "Universal Client — protocol-first, transport-agnostic",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -65,14 +65,14 @@
|
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@astrale-os/kernel-core": "0.9.0-beta.
|
|
69
|
-
"@astrale-os/kernel-dsl": "0.2.0-beta.
|
|
70
|
-
"@astrale-os/kernel-protocol": "0.5.0-beta.
|
|
68
|
+
"@astrale-os/kernel-core": "0.9.0-beta.56",
|
|
69
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.41",
|
|
70
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.62",
|
|
71
71
|
"@tanstack/pacer": "0.20.1",
|
|
72
72
|
"msgpackr": "^2.0.5"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@astrale-os/kernel-server": "0.5.0-beta.
|
|
75
|
+
"@astrale-os/kernel-server": "0.5.0-beta.64",
|
|
76
76
|
"@astrale/typescript-config": "npm:@jsr/astrale__typescript-config@~1.1.0",
|
|
77
77
|
"@types/node": "^22.0.0",
|
|
78
78
|
"typescript": "7.0.2",
|