@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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuthExchange, SessionAuth } from '../routing/index.js';
|
|
2
|
+
import type { ExchangeDependencies } from './exchange.js';
|
|
3
|
+
import type { SessionExchangeOptions } from './options.js';
|
|
4
|
+
/** The per-Call exchange a Session applies after resolving its source authority. */
|
|
5
|
+
export interface SessionExchange extends AuthExchange {
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Present each Call whose callable a remote installed Domain declares with the credential that
|
|
10
|
+
* Domain exchanged for the Call's source caller.
|
|
11
|
+
*
|
|
12
|
+
* Kernel callables, locally executed Domains, targets that name no Domain, and anonymous Calls keep
|
|
13
|
+
* the source authority unchanged. Domain credentials are held per source credential and replaced at
|
|
14
|
+
* the Session delegation threshold.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createSessionExchange(dependencies: ExchangeDependencies & {
|
|
17
|
+
readonly auth: SessionAuth;
|
|
18
|
+
}, options: Readonly<Required<SessionExchangeOptions>>): SessionExchange;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ClientError } from '../../errors/index.js';
|
|
2
|
+
import { runSessionOperation } from '../context/index.js';
|
|
3
|
+
import { createSessionCredentialProvider, delegationCoverageMs } from '../credential/index.js';
|
|
4
|
+
import { createInstallations, declaringOrigin, isKernelOrigin } from './domain.js';
|
|
5
|
+
import { exchangeDomainCredential, SELF_ATTENUATION } from './exchange.js';
|
|
6
|
+
import { MAXIMUM_EXCHANGE_SOURCES } from './limits.js';
|
|
7
|
+
import { isAuthority, renewing } from './source.js';
|
|
8
|
+
/**
|
|
9
|
+
* Present each Call whose callable a remote installed Domain declares with the credential that
|
|
10
|
+
* Domain exchanged for the Call's source caller.
|
|
11
|
+
*
|
|
12
|
+
* Kernel callables, locally executed Domains, targets that name no Domain, and anonymous Calls keep
|
|
13
|
+
* the source authority unchanged. Domain credentials are held per source credential and replaced at
|
|
14
|
+
* the Session delegation threshold.
|
|
15
|
+
*/
|
|
16
|
+
export function createSessionExchange(dependencies, options) {
|
|
17
|
+
const { auth } = dependencies;
|
|
18
|
+
const coverageMs = delegationCoverageMs(auth.ttlSeconds);
|
|
19
|
+
const installations = createInstallations(dependencies);
|
|
20
|
+
const sources = new Map();
|
|
21
|
+
return Object.freeze({ resolve, clear });
|
|
22
|
+
async function resolve(call, effective, operation) {
|
|
23
|
+
if (!isAuthority(effective))
|
|
24
|
+
return effective;
|
|
25
|
+
const origin = declaringOrigin(call.target);
|
|
26
|
+
if (origin === undefined || isKernelOrigin(origin))
|
|
27
|
+
return effective;
|
|
28
|
+
const domain = await installations.issuerOf(origin, effective, operation);
|
|
29
|
+
if (domain === undefined)
|
|
30
|
+
return effective;
|
|
31
|
+
return renewing(auth, call, effective, operation, async (source) => {
|
|
32
|
+
if (source.delegate.ttlSeconds > auth.ttlSeconds) {
|
|
33
|
+
throw new ClientError('Session delegation outlives the Domain credential it would carry.');
|
|
34
|
+
}
|
|
35
|
+
const provider = providerFor(source, domain);
|
|
36
|
+
const exchanged = await runSessionOperation(operation, (signal) => provider.acquire(signal));
|
|
37
|
+
return Object.freeze({ credential: exchanged.credential, delegate: source.delegate });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function clear() {
|
|
41
|
+
installations.clear();
|
|
42
|
+
sources.clear();
|
|
43
|
+
}
|
|
44
|
+
function providerFor(source, domain) {
|
|
45
|
+
const held = heldFor(source.credential);
|
|
46
|
+
const known = held.get(domain);
|
|
47
|
+
if (known !== undefined)
|
|
48
|
+
return known;
|
|
49
|
+
const provider = createSessionCredentialProvider({
|
|
50
|
+
ttlSeconds: auth.ttlSeconds,
|
|
51
|
+
mint: (signal) => exchangeDomainCredential(dependencies, source, {
|
|
52
|
+
issuer: domain,
|
|
53
|
+
ttlSeconds: options.ttlSeconds,
|
|
54
|
+
attenuation: SELF_ATTENUATION,
|
|
55
|
+
coverageMs,
|
|
56
|
+
}, dependencies.operation(signal)),
|
|
57
|
+
});
|
|
58
|
+
held.set(domain, provider);
|
|
59
|
+
return provider;
|
|
60
|
+
}
|
|
61
|
+
/** Domain credentials never cross source credentials; the oldest source is forgotten first. */
|
|
62
|
+
function heldFor(credential) {
|
|
63
|
+
const known = sources.get(credential);
|
|
64
|
+
if (known !== undefined)
|
|
65
|
+
return known;
|
|
66
|
+
const held = new Map();
|
|
67
|
+
sources.set(credential, held);
|
|
68
|
+
if (sources.size > MAXIMUM_EXCHANGE_SOURCES) {
|
|
69
|
+
sources.delete(sources.keys().next().value);
|
|
70
|
+
}
|
|
71
|
+
return held;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { invocation } from '@astrale-os/kernel-protocol';
|
|
2
|
+
import type { AuthCall } from '../../auth/auth.js';
|
|
3
|
+
import type { Call, Result } from '../../client/index.js';
|
|
4
|
+
import type { SessionOperation } from '../context/index.js';
|
|
5
|
+
import type { EffectiveAuth, SessionAuth } from '../routing/index.js';
|
|
6
|
+
import type { ExchangeDependencies } from './exchange.js';
|
|
7
|
+
import { ClientError } from '../../errors/index.js';
|
|
8
|
+
/** A resolved source that carries both a credential and its delegation. */
|
|
9
|
+
export type SourceAuthority = Required<EffectiveAuth>;
|
|
10
|
+
/** Source Kernel syscalls issued as exactly one admitted source caller within one operation. */
|
|
11
|
+
export interface SourceCaller {
|
|
12
|
+
dispatch(call: Call): Promise<Result>;
|
|
13
|
+
readonly call: AuthCall;
|
|
14
|
+
}
|
|
15
|
+
/** The source credential can no longer grant a delegation that covers what its exchange carries. */
|
|
16
|
+
export declare class SourceExhausted extends ClientError {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export declare function isAuthority(auth: EffectiveAuth): auth is SourceAuthority;
|
|
20
|
+
/** The source Kernel conversation of one exchange, acting as exactly the admitted source caller. */
|
|
21
|
+
export declare function sourceCaller(dependencies: Pick<ExchangeDependencies, 'source'>, auth: EffectiveAuth, operation: SessionOperation): SourceCaller;
|
|
22
|
+
/**
|
|
23
|
+
* Bound a requested delegation by the source credential's expiry less the issuance margin: the
|
|
24
|
+
* Kernel rejects a delegation that outlives its source, judged by its own clock. An opaque or
|
|
25
|
+
* transport-held source leaves the bound to the Kernel. A source that cannot grant one second, or
|
|
26
|
+
* bounds the delegation within `coverageMs`, is exhausted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function delegationTtl(credential: string | undefined, ttlSeconds: number, coverageMs?: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Run `attempt` with the resolved source, and once more with a renewed source if that one proved
|
|
31
|
+
* exhausted. The Session auth is refreshed only while it still resolves the exhausted credential, so
|
|
32
|
+
* concurrent Calls share one renewal; an auth that cannot refresh keeps the failure.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renewing<T>(auth: SessionAuth, call: invocation.Call, source: SourceAuthority, operation: SessionOperation, attempt: (source: SourceAuthority) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { inspect } from '@astrale-os/kernel-core/auth/credential';
|
|
2
|
+
import { ClientError } from '../../errors/index.js';
|
|
3
|
+
import { runSessionOperation, sessionTransportOptions } from '../context/index.js';
|
|
4
|
+
import { requireResultKind, resolveAuth } from '../routing/index.js';
|
|
5
|
+
import { ISSUANCE_MARGIN_SECONDS } from './limits.js';
|
|
6
|
+
/** The source credential can no longer grant a delegation that covers what its exchange carries. */
|
|
7
|
+
export class SourceExhausted extends ClientError {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('The source credential expires too soon for a Domain token exchange.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isAuthority(auth) {
|
|
13
|
+
return auth.credential !== undefined && auth.delegate !== undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The source Kernel conversation of one exchange, acting as exactly the admitted source caller. */
|
|
16
|
+
export function sourceCaller(dependencies, auth, operation) {
|
|
17
|
+
const client = dependencies.source();
|
|
18
|
+
const caller = auth.credential === undefined ? client : client.as(auth.credential);
|
|
19
|
+
const dispatch = (input) => runSessionOperation(operation, () => caller.dispatch(input, {
|
|
20
|
+
...sessionTransportOptions(operation),
|
|
21
|
+
...(auth.delegate === undefined ? {} : { delegate: auth.delegate }),
|
|
22
|
+
}));
|
|
23
|
+
const call = async (target, input) => requireResultKind('value', await dispatch({ target, input })).value;
|
|
24
|
+
return Object.freeze({ dispatch, call });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Bound a requested delegation by the source credential's expiry less the issuance margin: the
|
|
28
|
+
* Kernel rejects a delegation that outlives its source, judged by its own clock. An opaque or
|
|
29
|
+
* transport-held source leaves the bound to the Kernel. A source that cannot grant one second, or
|
|
30
|
+
* bounds the delegation within `coverageMs`, is exhausted.
|
|
31
|
+
*/
|
|
32
|
+
export function delegationTtl(credential, ttlSeconds, coverageMs = 0) {
|
|
33
|
+
const remaining = remainingSeconds(credential);
|
|
34
|
+
if (remaining === undefined || remaining >= ttlSeconds)
|
|
35
|
+
return ttlSeconds;
|
|
36
|
+
if (remaining < 1 || remaining * 1_000 <= coverageMs)
|
|
37
|
+
throw new SourceExhausted();
|
|
38
|
+
return remaining;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run `attempt` with the resolved source, and once more with a renewed source if that one proved
|
|
42
|
+
* exhausted. The Session auth is refreshed only while it still resolves the exhausted credential, so
|
|
43
|
+
* concurrent Calls share one renewal; an auth that cannot refresh keeps the failure.
|
|
44
|
+
*/
|
|
45
|
+
export async function renewing(auth, call, source, operation, attempt) {
|
|
46
|
+
try {
|
|
47
|
+
return await attempt(source);
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
if (!(cause instanceof SourceExhausted))
|
|
51
|
+
throw cause;
|
|
52
|
+
let renewed = await resolveAuth(auth, call, operation);
|
|
53
|
+
if (renewed.credential === source.credential && auth.refresh !== undefined) {
|
|
54
|
+
const refresh = auth.refresh;
|
|
55
|
+
await runSessionOperation(operation, async (signal) => {
|
|
56
|
+
await refresh(signal);
|
|
57
|
+
});
|
|
58
|
+
renewed = await resolveAuth(auth, call, operation);
|
|
59
|
+
}
|
|
60
|
+
if (!isAuthority(renewed) || renewed.credential === source.credential)
|
|
61
|
+
throw cause;
|
|
62
|
+
return attempt(renewed);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function remainingSeconds(credential) {
|
|
66
|
+
if (credential === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
let expiresAt;
|
|
69
|
+
try {
|
|
70
|
+
expiresAt = inspect(credential).claims.exp;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt))
|
|
76
|
+
return undefined;
|
|
77
|
+
return expiresAt - Math.ceil(Date.now() / 1_000) - ISSUANCE_MARGIN_SECONDS;
|
|
78
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { NodeId } from '@astrale-os/kernel-core/graph/node';
|
|
2
|
+
import type { Value } from '@astrale-os/kernel-dsl/value';
|
|
3
|
+
import type { CallableReference } from './reference.js';
|
|
4
|
+
declare const EXECUTION_OUTPUT: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* Immediate value of a durable call: the `K.Execution` it created or recovered. `Output` is the
|
|
7
|
+
* handler output of the called declaration, carried only by the type.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExecutionRef<Output = unknown> {
|
|
10
|
+
readonly id: NodeId;
|
|
11
|
+
readonly [EXECUTION_OUTPUT]?: Output;
|
|
12
|
+
}
|
|
13
|
+
export type ExecutionOutcome<Output = unknown> = {
|
|
14
|
+
readonly kind: 'completed';
|
|
15
|
+
readonly value: Output;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'failed';
|
|
18
|
+
readonly error: {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly family?: string;
|
|
22
|
+
readonly details?: Value;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'cancelled';
|
|
26
|
+
};
|
|
27
|
+
export type ExecutionState = 'pending' | 'running' | 'waiting' | 'paused' | 'completed' | 'failed' | 'cancelled' | 'expired';
|
|
28
|
+
export interface ExecutionStatus<Output = unknown> {
|
|
29
|
+
readonly id: NodeId;
|
|
30
|
+
readonly function: NodeId;
|
|
31
|
+
readonly version: `sha256:${string}`;
|
|
32
|
+
readonly state: ExecutionState;
|
|
33
|
+
readonly createdAt: string;
|
|
34
|
+
readonly outcome?: ExecutionOutcome<Output>;
|
|
35
|
+
}
|
|
36
|
+
type Empty = Readonly<Record<string, never>>;
|
|
37
|
+
type ExecutionLike<Output> = ExecutionRef<Output> | NodeId;
|
|
38
|
+
/**
|
|
39
|
+
* Typed references to the methods of one Execution, for `session.invoke`:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const run = await session.invoke(remind(invoice), { days: 3 })
|
|
43
|
+
* const outcome = await session.invoke(execution.result(run), { waitMs: 10_000 })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const execution: Readonly<{
|
|
47
|
+
status: <Output>(target: ExecutionLike<Output>) => CallableReference<Empty, ExecutionStatus<Output>>;
|
|
48
|
+
/** Outcome once terminal; fails `EXECUTION_PENDING` when `waitMs` elapses first. */
|
|
49
|
+
result: <Output>(target: ExecutionLike<Output>) => CallableReference<{
|
|
50
|
+
readonly waitMs?: number;
|
|
51
|
+
}, ExecutionOutcome<Output>>;
|
|
52
|
+
/** Cooperative cancellation; `force` kills without compensation (owner Domain only). */
|
|
53
|
+
cancel: <Output>(target: ExecutionLike<Output>) => CallableReference<{
|
|
54
|
+
readonly force?: boolean;
|
|
55
|
+
}, ExecutionStatus<Output>>;
|
|
56
|
+
/** Resolve a named durable promise once (owner Domain only). */
|
|
57
|
+
signal: (target: ExecutionLike<unknown>) => CallableReference<{
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly value: Value;
|
|
60
|
+
}, Empty>;
|
|
61
|
+
resume: <Output>(target: ExecutionLike<Output>) => CallableReference<Empty, ExecutionStatus<Output>>;
|
|
62
|
+
/** Forget the Execution and stop its run (owner Domain only). */
|
|
63
|
+
delete: (target: ExecutionLike<unknown>) => CallableReference<Empty, Empty>;
|
|
64
|
+
}>;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
|
+
import { K } from '@astrale-os/kernel-core/schema';
|
|
3
|
+
import { reference } from './reference.js';
|
|
4
|
+
const methods = K.classes.Execution.methods;
|
|
5
|
+
const bound = {
|
|
6
|
+
status: reference(K, methods.status),
|
|
7
|
+
result: reference(K, methods.result),
|
|
8
|
+
cancel: reference(K, methods.cancel),
|
|
9
|
+
signal: reference(K, methods.signal),
|
|
10
|
+
resume: reference(K, methods.resume),
|
|
11
|
+
delete: reference(K, methods.delete),
|
|
12
|
+
};
|
|
13
|
+
function receiver(execution) {
|
|
14
|
+
return Path.id(typeof execution === 'string' ? execution : execution.id);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Typed references to the methods of one Execution, for `session.invoke`:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const run = await session.invoke(remind(invoice), { days: 3 })
|
|
21
|
+
* const outcome = await session.invoke(execution.result(run), { waitMs: 10_000 })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const execution = Object.freeze({
|
|
25
|
+
status: (target) => bound.status(receiver(target)),
|
|
26
|
+
/** Outcome once terminal; fails `EXECUTION_PENDING` when `waitMs` elapses first. */
|
|
27
|
+
result: (target) => bound.result(receiver(target)),
|
|
28
|
+
/** Cooperative cancellation; `force` kills without compensation (owner Domain only). */
|
|
29
|
+
cancel: (target) => bound.cancel(receiver(target)),
|
|
30
|
+
/** Resolve a named durable promise once (owner Domain only). */
|
|
31
|
+
signal: (target) => bound.signal(receiver(target)),
|
|
32
|
+
resume: (target) => bound.resume(receiver(target)),
|
|
33
|
+
/** Forget the Execution and stop its run (owner Domain only). */
|
|
34
|
+
delete: (target) => bound.delete(receiver(target)),
|
|
35
|
+
});
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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 {
|
|
6
|
+
export type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
7
|
+
export type { Auth, PersistedRoute, SessionRouteArtifact, SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody, } from './routing/index.js';
|
|
6
8
|
export { ClientSession, connect, type ClientSessionOptions, type ConnectOptions, } from './session.js';
|
|
7
|
-
export type { CallableReference, InputOfReference, InstanceCallableReference, OutputOfReference, } from './reference.js';
|
|
9
|
+
export type { CallableReference, InputOfReference, InputModeOfReference, InstanceCallableReference, OutputOfReference, } from './reference.js';
|
|
8
10
|
export { reference } from './reference.js';
|
|
11
|
+
export { execution, type ExecutionOutcome, type ExecutionRef, type ExecutionState, type ExecutionStatus, } from './execution.js';
|
|
9
12
|
export type { ResolvedView, ViewCatalog } from './views/index.js';
|
|
10
13
|
export type { DomainBinding } from '../graph/domain/index.js';
|
package/dist/session/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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
|
+
import type { ExecutionRef } from './execution.js';
|
|
7
8
|
declare const REFERENCE_INPUT: unique symbol;
|
|
9
|
+
declare const REFERENCE_INPUT_MODE: unique symbol;
|
|
8
10
|
declare const REFERENCE_OUTPUT: unique symbol;
|
|
9
|
-
export interface CallableReference<InputValue = Input, Output = unknown> {
|
|
11
|
+
export interface CallableReference<InputValue = Input, Output = unknown, InputMode extends 'value' | 'binary' = 'value' | 'binary'> {
|
|
10
12
|
readonly origin: KeyType.Origin;
|
|
11
13
|
/** @deprecated Informational only; dispatch pins `fingerprint` and revision pinning will be removed. */
|
|
12
14
|
readonly revision: Revision;
|
|
@@ -16,6 +18,7 @@ export interface CallableReference<InputValue = Input, Output = unknown> {
|
|
|
16
18
|
readonly target: PathValue;
|
|
17
19
|
readonly outputMode: 'value' | 'stream' | 'binary';
|
|
18
20
|
readonly [REFERENCE_INPUT]?: InputValue;
|
|
21
|
+
readonly [REFERENCE_INPUT_MODE]?: InputMode;
|
|
19
22
|
readonly [REFERENCE_OUTPUT]?: Output;
|
|
20
23
|
}
|
|
21
24
|
export interface InstanceCallableReference<InputValue = Input, Output = unknown> {
|
|
@@ -28,12 +31,18 @@ export interface InstanceCallableReference<InputValue = Input, Output = unknown>
|
|
|
28
31
|
}
|
|
29
32
|
export type InputOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer InputValue, unknown> ? InputValue : never;
|
|
30
33
|
export type OutputOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer Output> ? Output : never;
|
|
34
|
+
export type InputModeOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer _Output, infer Mode> ? Mode : never;
|
|
31
35
|
type ReferenceInput<Callable> = unknown extends CallableInputOf<Callable> ? Input : CallableInputOf<Callable>;
|
|
32
|
-
type
|
|
36
|
+
type HandlerOutput<Callable> = unknown extends CallableOutputOf<Callable> ? unknown : CallableResultOf<Callable, Binary>;
|
|
37
|
+
type ReferenceOutput<Callable> = Callable extends {
|
|
38
|
+
readonly execution: {
|
|
39
|
+
readonly durable: true;
|
|
40
|
+
};
|
|
41
|
+
} ? ExecutionRef<HandlerOutput<Callable>> : HandlerOutput<Callable>;
|
|
33
42
|
type ExecutableStaticMethod = ExecutableMethod<unknown, unknown, true>;
|
|
34
43
|
type ExecutableInstanceMethod = ExecutableMethod<unknown, unknown, false>;
|
|
35
44
|
/** 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>>;
|
|
45
|
+
export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>, CallableInputModeOf<Callable>>;
|
|
37
46
|
/** Project one exact resolved instance Method into a receiver-bound typed Client reference. */
|
|
38
47
|
export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin' | 'revision'>, callable: Callable): InstanceCallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
|
|
39
48
|
export declare function admitReference(input: CallableReference<unknown>): CallableReference<unknown>;
|
|
@@ -9,10 +9,20 @@ export interface SessionAuth {
|
|
|
9
9
|
/** Default TTL for authenticated Calls whose Auth omits a delegation override. */
|
|
10
10
|
readonly ttlSeconds: number;
|
|
11
11
|
resolve(call: invocation.Call, signal: AbortSignal): Auth | Promise<Auth>;
|
|
12
|
+
/**
|
|
13
|
+
* Replace the source credential now, before `resolve` would. A Session asks for it only when a
|
|
14
|
+
* credential it derives from the current source, such as a Domain exchange, could not cover
|
|
15
|
+
* `ttlSeconds`: a derived credential never outlives its source.
|
|
16
|
+
*/
|
|
17
|
+
refresh?(signal: AbortSignal): PromiseLike<unknown> | void;
|
|
12
18
|
}
|
|
13
19
|
export interface EffectiveAuth {
|
|
14
20
|
readonly credential?: string;
|
|
15
21
|
readonly delegate?: invocation.Delegate;
|
|
16
22
|
}
|
|
23
|
+
/** Replace the resolved source authority of one Call before routing partitions and presents it. */
|
|
24
|
+
export interface AuthExchange {
|
|
25
|
+
resolve(call: invocation.Call, auth: EffectiveAuth, operation: SessionOperation): Promise<EffectiveAuth>;
|
|
26
|
+
}
|
|
17
27
|
export declare function admitSessionAuth(input: SessionAuth | undefined): SessionAuth | undefined;
|
|
18
28
|
export declare function resolveAuth(resolver: SessionAuth | undefined, call: invocation.Call, operation: SessionOperation): Promise<EffectiveAuth>;
|
|
@@ -10,7 +10,14 @@ export function admitSessionAuth(input) {
|
|
|
10
10
|
if (!Number.isSafeInteger(input.ttlSeconds) || input.ttlSeconds < 1) {
|
|
11
11
|
throw new TypeError('Session auth ttlSeconds must be a positive safe integer.');
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
if (input.refresh !== undefined && typeof input.refresh !== 'function') {
|
|
14
|
+
throw new TypeError('Session auth refresh capability is invalid.');
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
ttlSeconds: input.ttlSeconds,
|
|
18
|
+
resolve: input.resolve.bind(input),
|
|
19
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh.bind(input) }),
|
|
20
|
+
});
|
|
14
21
|
}
|
|
15
22
|
export async function resolveAuth(resolver, call, operation) {
|
|
16
23
|
if (resolver === undefined)
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
import type { SessionAuth } from './auth.js';
|
|
5
|
+
import type { AuthExchange, SessionAuth } from './auth.js';
|
|
5
6
|
import type { RouteCache } from './cache.js';
|
|
6
7
|
import type { ClientPoolCapability } from './envelope.js';
|
|
7
8
|
import type { SessionEnvelopeTransport, SessionRequestOptions } from './request.js';
|
|
@@ -12,19 +13,29 @@ export interface SessionDispatcherOptions {
|
|
|
12
13
|
readonly pool: ClientPoolCapability;
|
|
13
14
|
readonly websocketAvailable: boolean;
|
|
14
15
|
readonly auth?: SessionAuth;
|
|
16
|
+
readonly exchange?: AuthExchange;
|
|
15
17
|
readonly cache: RouteCache;
|
|
16
18
|
readonly policy: Readonly<SessionPolicy>;
|
|
17
19
|
readonly envelopeTransport: SessionEnvelopeTransport;
|
|
18
20
|
readonly requireOpen: () => void;
|
|
21
|
+
readonly streamingHttp: {
|
|
22
|
+
readonly fetch: Fetch;
|
|
23
|
+
readonly maximumResponseBytes: number;
|
|
24
|
+
readonly timeoutMs: number;
|
|
25
|
+
};
|
|
19
26
|
}
|
|
20
27
|
export declare class SessionDispatcher {
|
|
21
28
|
private readonly state;
|
|
22
29
|
constructor(state: SessionDispatcherOptions);
|
|
23
30
|
dispatch(input: Parameters<typeof acceptCall>[0], options: SessionRequestOptions, operation: SessionOperation): Promise<Result>;
|
|
31
|
+
/** Reuse Session authority and route recovery while forcing HTTP for streaming calls. */
|
|
32
|
+
dispatchHttp(input: Parameters<typeof acceptCall>[0], options: SessionRequestOptions, operation: SessionOperation, body?: Blob): Promise<Result>;
|
|
33
|
+
private dispatchWith;
|
|
24
34
|
private dispatchSource;
|
|
25
35
|
private followSource;
|
|
26
36
|
private dispatchDestinationWithRecovery;
|
|
27
37
|
private dispatchDestination;
|
|
38
|
+
private streamingClientFor;
|
|
28
39
|
}
|
|
29
40
|
export declare function requireResultKind<Kind extends 'value' | 'stream' | 'binary'>(expected: Kind, result: Result): Extract<Result, {
|
|
30
41
|
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,11 +11,21 @@ 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
|
|
17
|
-
// carrier minted for a different caller partition.
|
|
18
|
-
const
|
|
24
|
+
// carrier minted for a different caller partition. An exchanged credential is its own partition.
|
|
25
|
+
const resolved = await resolveAuth(this.state.auth, call, operation);
|
|
26
|
+
const auth = this.state.exchange === undefined
|
|
27
|
+
? resolved
|
|
28
|
+
: await this.state.exchange.resolve(call, resolved, operation);
|
|
19
29
|
const source = this.state.kernel;
|
|
20
30
|
const cacheKey = await this.state.cache.key(source, call, auth);
|
|
21
31
|
const dispatch = Object.freeze({
|
|
@@ -27,7 +37,7 @@ export class SessionDispatcher {
|
|
|
27
37
|
let learned = await this.state.cache.get(cacheKey, auth.credential);
|
|
28
38
|
if (learned !== undefined) {
|
|
29
39
|
try {
|
|
30
|
-
requireCachedRoute(learned, source, call.target, this.state.envelopeTransport, this.state.websocketAvailable, this.state.policy);
|
|
40
|
+
requireCachedRoute(learned, source, call.target, streaming === undefined ? this.state.envelopeTransport : 'http', this.state.websocketAvailable, this.state.policy);
|
|
31
41
|
}
|
|
32
42
|
catch {
|
|
33
43
|
this.state.cache.delete(cacheKey);
|
|
@@ -35,22 +45,31 @@ export class SessionDispatcher {
|
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
if (learned !== undefined) {
|
|
38
|
-
return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation);
|
|
48
|
+
return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation, undefined, streaming);
|
|
39
49
|
}
|
|
40
|
-
const result = await this.dispatchSource(dispatch, undefined, options, operation);
|
|
41
|
-
return this.followSource(result, dispatch, options, operation);
|
|
50
|
+
const result = await this.dispatchSource(dispatch, undefined, options, operation, streaming);
|
|
51
|
+
return this.followSource(result, dispatch, options, operation, streaming);
|
|
42
52
|
}
|
|
43
|
-
async dispatchSource(state, routing, options, operation) {
|
|
53
|
+
async dispatchSource(state, routing, options, operation, streaming) {
|
|
44
54
|
this.state.requireOpen();
|
|
45
55
|
allowEndpoint(this.state.sourceEndpoint, this.state.policy);
|
|
46
|
-
const client =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const client = streaming === undefined
|
|
57
|
+
? this.state.pool.clientFor(Object.freeze({ transport: 'http', url: this.state.sourceEndpoint }))
|
|
58
|
+
: this.streamingClientFor(this.state.sourceEndpoint);
|
|
59
|
+
return runSessionOperation(operation, () => streaming?.body === undefined
|
|
60
|
+
? authenticated(client, state.auth.credential).dispatch(state.call, {
|
|
61
|
+
...sessionTransportOptions(operation, options.idempotencyKey),
|
|
62
|
+
...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
|
|
63
|
+
...(routing === undefined ? {} : { routing }),
|
|
64
|
+
})
|
|
65
|
+
: authenticated(client, state.auth.credential).dispatch(state.call, {
|
|
66
|
+
...sessionTransportOptions(operation),
|
|
67
|
+
...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
|
|
68
|
+
...(routing === undefined ? {} : { routing }),
|
|
69
|
+
body: streaming.body,
|
|
70
|
+
}));
|
|
52
71
|
}
|
|
53
|
-
async followSource(result, state, options, operation) {
|
|
72
|
+
async followSource(result, state, options, operation, streaming) {
|
|
54
73
|
if (result.kind !== 'redirect')
|
|
55
74
|
return result;
|
|
56
75
|
const redirect = result.redirect;
|
|
@@ -59,11 +78,11 @@ export class SessionDispatcher {
|
|
|
59
78
|
}
|
|
60
79
|
requireTargetPublication(redirect.route, state.call.target);
|
|
61
80
|
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);
|
|
81
|
+
return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace, streaming);
|
|
63
82
|
}
|
|
64
|
-
async dispatchDestinationWithRecovery(learned, state, options, operation, trace) {
|
|
83
|
+
async dispatchDestinationWithRecovery(learned, state, options, operation, trace, streaming) {
|
|
65
84
|
try {
|
|
66
|
-
return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace);
|
|
85
|
+
return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace, streaming);
|
|
67
86
|
}
|
|
68
87
|
catch (cause) {
|
|
69
88
|
const stale = isAdmittedStaleRouteError(cause);
|
|
@@ -83,20 +102,31 @@ export class SessionDispatcher {
|
|
|
83
102
|
stale: learned.route.via.publication.etag,
|
|
84
103
|
})
|
|
85
104
|
: undefined;
|
|
86
|
-
const refreshed = await this.dispatchSource(state, routing, options, operation);
|
|
87
|
-
return this.followSource(refreshed, state, options, operation);
|
|
105
|
+
const refreshed = await this.dispatchSource(state, routing, options, operation, streaming);
|
|
106
|
+
return this.followSource(refreshed, state, options, operation, streaming);
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
|
-
async dispatchDestination(learned, call, options, operation, source, trace) {
|
|
109
|
+
async dispatchDestination(learned, call, options, operation, source, trace, streaming) {
|
|
91
110
|
if (learned.route.via.issuer !== source) {
|
|
92
111
|
throw new ClientError('Learned route issuer differs from the session-pinned source Kernel.');
|
|
93
112
|
}
|
|
94
113
|
requireTargetPublication(learned.route, call.target);
|
|
95
|
-
const
|
|
114
|
+
const destination = streaming === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: selectEnvelopeTarget(learned.route.endpoint, 'http', this.state.websocketAvailable, this.state.policy);
|
|
117
|
+
const client = streaming === undefined
|
|
118
|
+
? selectEnvelope(learned.route.endpoint, this.state.envelopeTransport, this.state.websocketAvailable, this.state.pool, this.state.policy).client
|
|
119
|
+
: destination?.transport === 'http'
|
|
120
|
+
? this.streamingClientFor(destination.url)
|
|
121
|
+
: invalidStreamingDestination();
|
|
96
122
|
const requestOptions = {
|
|
97
123
|
...sessionTransportOptions(operation, options.idempotencyKey),
|
|
98
124
|
routing: learned.route.via,
|
|
99
125
|
};
|
|
126
|
+
const binaryRequestOptions = {
|
|
127
|
+
...sessionTransportOptions(operation),
|
|
128
|
+
routing: learned.route.via,
|
|
129
|
+
};
|
|
100
130
|
// The source has admitted this fingerprint against the exact Publication carried by Via.
|
|
101
131
|
// Pin that revision at the destination so existing Workers can execute the same admitted
|
|
102
132
|
// callable without understanding the newer fingerprint Target representation. The cache
|
|
@@ -111,14 +141,32 @@ export class SessionDispatcher {
|
|
|
111
141
|
}),
|
|
112
142
|
}
|
|
113
143
|
: call;
|
|
114
|
-
const result = await runSessionOperation(operation, () =>
|
|
115
|
-
?
|
|
116
|
-
|
|
144
|
+
const result = await runSessionOperation(operation, () => streaming?.body !== undefined
|
|
145
|
+
? trace === undefined
|
|
146
|
+
? authenticated(client, learned.credential).dispatch(destinationCall, {
|
|
147
|
+
...binaryRequestOptions,
|
|
148
|
+
body: streaming.body,
|
|
149
|
+
})
|
|
150
|
+
: dispatchTrace(client, learned.credential, destinationCall, { ...binaryRequestOptions, body: streaming.body }, trace)
|
|
151
|
+
: trace === undefined
|
|
152
|
+
? authenticated(client, learned.credential).dispatch(destinationCall, requestOptions)
|
|
153
|
+
: dispatchTrace(client, learned.credential, destinationCall, requestOptions, trace));
|
|
117
154
|
if (result.kind === 'redirect') {
|
|
118
155
|
throw new ClientError('A destination redirect is invalid; only the source Kernel may redirect.');
|
|
119
156
|
}
|
|
120
157
|
return result;
|
|
121
158
|
}
|
|
159
|
+
streamingClientFor(url) {
|
|
160
|
+
return new Client({
|
|
161
|
+
url,
|
|
162
|
+
fetch: this.state.streamingHttp.fetch,
|
|
163
|
+
timeoutMs: this.state.streamingHttp.timeoutMs,
|
|
164
|
+
maximumResponseBytes: this.state.streamingHttp.maximumResponseBytes,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function invalidStreamingDestination() {
|
|
169
|
+
throw new ClientError('Streaming route has no HTTP endpoint.');
|
|
122
170
|
}
|
|
123
171
|
function requireCachedRoute(learned, source, target, envelopeTransport, websocketAvailable, policy) {
|
|
124
172
|
if (learned.route.via.issuer !== source) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { admitSessionAuth, resolveAuth, type Auth, type EffectiveAuth, type SessionAuth, } from './auth.js';
|
|
1
|
+
export { admitSessionAuth, resolveAuth, type Auth, type AuthExchange, type EffectiveAuth, type SessionAuth, } from './auth.js';
|
|
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';
|