@astrale-os/kernel-client 0.6.0-beta.72 → 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 +1 -0
- package/dist/auth/credential.js +2 -2
- 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/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 +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/reference.d.ts +7 -1
- package/dist/session/routing/auth.d.ts +10 -0
- package/dist/session/routing/auth.js +8 -1
- package/dist/session/routing/dispatch.d.ts +2 -1
- package/dist/session/routing/dispatch.js +5 -2
- package/dist/session/routing/index.d.ts +1 -1
- package/dist/session/session.d.ts +16 -0
- package/dist/session/session.js +24 -4
- package/dist/session/session.typecheck.js +32 -1
- package/dist/transport/http/transport.js +0 -1
- package/package.json +5 -5
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { accept as acceptIssuer } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
|
+
import { admitControlOptions } from '../context/index.js';
|
|
3
|
+
import { delegationCoverageMs } from '../credential/index.js';
|
|
4
|
+
import { DEFAULT_EXCHANGE_TTL_SECONDS } from './limits.js';
|
|
5
|
+
/**
|
|
6
|
+
* Admit exchange options against the Session auth they extend. Every Domain credential must still
|
|
7
|
+
* cover the Session delegation, so a request that could never cover it is rejected.
|
|
8
|
+
*/
|
|
9
|
+
export function admitExchangeOptions(input, auth) {
|
|
10
|
+
if (input === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
13
|
+
throw new TypeError('Session exchange options are invalid.');
|
|
14
|
+
}
|
|
15
|
+
if (Reflect.ownKeys(input).some((key) => key !== 'ttlSeconds')) {
|
|
16
|
+
throw new TypeError('Session exchange options contain unexpected fields.');
|
|
17
|
+
}
|
|
18
|
+
if (auth === undefined)
|
|
19
|
+
throw new TypeError('Session exchange requires Session auth.');
|
|
20
|
+
const ttlSeconds = admitTtlSeconds(input.ttlSeconds);
|
|
21
|
+
if (ttlSeconds * 1_000 <= delegationCoverageMs(auth.ttlSeconds)) {
|
|
22
|
+
throw new TypeError('Session exchange ttlSeconds must exceed the Session auth ttlSeconds.');
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze({ ttlSeconds });
|
|
25
|
+
}
|
|
26
|
+
/** Admit an explicit exchange: the canonical absolute issuer URL of the named Domain and its lifetime. */
|
|
27
|
+
export function admitExchangeRequest(domain, input) {
|
|
28
|
+
if (input !== undefined &&
|
|
29
|
+
(input === null || typeof input !== 'object' || Array.isArray(input))) {
|
|
30
|
+
throw new TypeError('Session exchange request is invalid.');
|
|
31
|
+
}
|
|
32
|
+
const ttlSeconds = admitTtlSeconds(input?.ttlSeconds);
|
|
33
|
+
const control = admitControlOptions(input === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: {
|
|
36
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
37
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
38
|
+
});
|
|
39
|
+
return Object.freeze({ issuer: admitDomainIssuer(domain), ttlSeconds, control });
|
|
40
|
+
}
|
|
41
|
+
function admitTtlSeconds(input) {
|
|
42
|
+
const ttlSeconds = input ?? DEFAULT_EXCHANGE_TTL_SECONDS;
|
|
43
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1) {
|
|
44
|
+
throw new TypeError('Session exchange ttlSeconds must be a positive safe integer.');
|
|
45
|
+
}
|
|
46
|
+
return ttlSeconds;
|
|
47
|
+
}
|
|
48
|
+
function admitDomainIssuer(input) {
|
|
49
|
+
try {
|
|
50
|
+
const issuer = acceptIssuer(input);
|
|
51
|
+
void new URL(issuer);
|
|
52
|
+
return issuer;
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
throw new TypeError('Domain issuer is invalid.', { cause });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -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
|
@@ -3,9 +3,11 @@ export type { ContentApi, ContentLocation, ContentUploadOptions, ContentUploadRe
|
|
|
3
3
|
export { bindSession, type BoundClientSession, type BoundSessionControls, type BoundSessionSource, } from './bound.js';
|
|
4
4
|
export { createSessionCredentialProvider, type SessionCredential, type SessionCredentialProvider, type SessionCredentialProviderOptions, type SessionCredentialState, } from './credential/index.js';
|
|
5
5
|
export type { SessionSnapshot } from './discovery/index.js';
|
|
6
|
+
export type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
6
7
|
export type { Auth, PersistedRoute, SessionRouteArtifact, SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody, } from './routing/index.js';
|
|
7
8
|
export { ClientSession, connect, type ClientSessionOptions, type ConnectOptions, } from './session.js';
|
|
8
9
|
export type { CallableReference, InputOfReference, InputModeOfReference, InstanceCallableReference, OutputOfReference, } from './reference.js';
|
|
9
10
|
export { reference } from './reference.js';
|
|
11
|
+
export { execution, type ExecutionOutcome, type ExecutionRef, type ExecutionState, type ExecutionStatus, } from './execution.js';
|
|
10
12
|
export type { ResolvedView, ViewCatalog } from './views/index.js';
|
|
11
13
|
export type { DomainBinding } from '../graph/domain/index.js';
|
package/dist/session/index.js
CHANGED
|
@@ -4,6 +4,7 @@ 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;
|
|
8
9
|
declare const REFERENCE_INPUT_MODE: unique symbol;
|
|
9
10
|
declare const REFERENCE_OUTPUT: unique symbol;
|
|
@@ -32,7 +33,12 @@ export type InputOfReference<Reference extends CallableReference<unknown>> = Ref
|
|
|
32
33
|
export type OutputOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer Output> ? Output : never;
|
|
33
34
|
export type InputModeOfReference<Reference extends CallableReference<unknown>> = Reference extends CallableReference<infer _Input, infer _Output, infer Mode> ? Mode : never;
|
|
34
35
|
type ReferenceInput<Callable> = unknown extends CallableInputOf<Callable> ? Input : CallableInputOf<Callable>;
|
|
35
|
-
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>;
|
|
36
42
|
type ExecutableStaticMethod = ExecutableMethod<unknown, unknown, true>;
|
|
37
43
|
type ExecutableInstanceMethod = ExecutableMethod<unknown, unknown, false>;
|
|
38
44
|
/** Project one exact resolved Function or static Method into a typed Client reference. */
|
|
@@ -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)
|
|
@@ -2,7 +2,7 @@ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
|
2
2
|
import type { Result } from '../../client/index.js';
|
|
3
3
|
import type { Fetch } from '../../transport/http/index.js';
|
|
4
4
|
import type { SessionOperation, SessionPolicy } from '../context/index.js';
|
|
5
|
-
import type { SessionAuth } from './auth.js';
|
|
5
|
+
import type { AuthExchange, SessionAuth } from './auth.js';
|
|
6
6
|
import type { RouteCache } from './cache.js';
|
|
7
7
|
import type { ClientPoolCapability } from './envelope.js';
|
|
8
8
|
import type { SessionEnvelopeTransport, SessionRequestOptions } from './request.js';
|
|
@@ -13,6 +13,7 @@ export interface SessionDispatcherOptions {
|
|
|
13
13
|
readonly pool: ClientPoolCapability;
|
|
14
14
|
readonly websocketAvailable: boolean;
|
|
15
15
|
readonly auth?: SessionAuth;
|
|
16
|
+
readonly exchange?: AuthExchange;
|
|
16
17
|
readonly cache: RouteCache;
|
|
17
18
|
readonly policy: Readonly<SessionPolicy>;
|
|
18
19
|
readonly envelopeTransport: SessionEnvelopeTransport;
|
|
@@ -21,8 +21,11 @@ export class SessionDispatcher {
|
|
|
21
21
|
this.state.requireOpen();
|
|
22
22
|
const call = acceptCall(input);
|
|
23
23
|
// Authority is resolved before warm lookup so changed credentials or delegation cannot reuse a
|
|
24
|
-
// carrier minted for a different caller partition.
|
|
25
|
-
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);
|
|
26
29
|
const source = this.state.kernel;
|
|
27
30
|
const cacheKey = await this.state.cache.key(source, call, auth);
|
|
28
31
|
const dispatch = Object.freeze({
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -12,7 +12,9 @@ import type { Fetch } from '../transport/http/index.js';
|
|
|
12
12
|
import type { WebSocketConstructor } from '../transport/websocket/index.js';
|
|
13
13
|
import type { ContentApi } from './content.js';
|
|
14
14
|
import type { SessionControlOptions, SessionPolicy } from './context/index.js';
|
|
15
|
+
import type { SessionCredential } from './credential/index.js';
|
|
15
16
|
import type { SessionSnapshot } from './discovery/index.js';
|
|
17
|
+
import type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
16
18
|
import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
|
|
17
19
|
import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
|
|
18
20
|
import type { ViewCatalog } from './views/index.js';
|
|
@@ -24,6 +26,12 @@ export interface ClientSessionOptions {
|
|
|
24
26
|
readonly pool?: Pick<ClientPoolType, 'clientFor'>;
|
|
25
27
|
readonly websocket?: WebSocketConstructor;
|
|
26
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;
|
|
27
35
|
/** Optional caller-owned persistence for bounded confidential route artifacts. */
|
|
28
36
|
readonly routeStore?: SessionRouteStore;
|
|
29
37
|
readonly policy: SessionPolicy;
|
|
@@ -52,11 +60,19 @@ export declare class ClientSession {
|
|
|
52
60
|
private readonly lifecycle;
|
|
53
61
|
private readonly timeoutMs;
|
|
54
62
|
private readonly routes;
|
|
63
|
+
private readonly exchangeDependencies;
|
|
64
|
+
private readonly callExchange?;
|
|
55
65
|
private readonly admitTargetViews;
|
|
56
66
|
private closed;
|
|
57
67
|
constructor(options: ClientSessionOptions);
|
|
58
68
|
/** Bind one admitted DSL Domain to authenticated graph projection and Method dispatch. */
|
|
59
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>;
|
|
60
76
|
/** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
|
|
61
77
|
publication(options?: SessionControlOptions): Promise<publication.Publication>;
|
|
62
78
|
snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
|
package/dist/session/session.js
CHANGED
|
@@ -14,6 +14,7 @@ import { admitFunction } from './admission.js';
|
|
|
14
14
|
import { createContentApi } from './content.js';
|
|
15
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';
|
|
16
16
|
import { createSessionDiscovery } from './discovery/index.js';
|
|
17
|
+
import { acquireDomainCredential, admitExchangeOptions, admitExchangeRequest, createSessionExchange, } from './exchange/index.js';
|
|
17
18
|
import { admitReference } from './reference.js';
|
|
18
19
|
import { admitEnvelopeTransport, admitInvocationOptions, admitSessionAuth, requireResultKind, RouteCache, SessionDispatcher, } from './routing/index.js';
|
|
19
20
|
import { resolveViewsFor, targetViewCatalog } from './views/index.js';
|
|
@@ -31,6 +32,8 @@ export class ClientSession {
|
|
|
31
32
|
lifecycle = new AbortController();
|
|
32
33
|
timeoutMs;
|
|
33
34
|
routes;
|
|
35
|
+
exchangeDependencies;
|
|
36
|
+
callExchange;
|
|
34
37
|
admitTargetViews;
|
|
35
38
|
closed = false;
|
|
36
39
|
constructor(options) {
|
|
@@ -86,24 +89,31 @@ export class ClientSession {
|
|
|
86
89
|
});
|
|
87
90
|
this.routes = new RouteCache(policy.maximumRouteAgeMs, undefined, options.routeStore);
|
|
88
91
|
const auth = admitSessionAuth(options.auth);
|
|
89
|
-
const
|
|
92
|
+
const exchange = admitExchangeOptions(options.exchange, auth);
|
|
93
|
+
this.exchangeDependencies = Object.freeze({
|
|
90
94
|
kernel: this.kernel,
|
|
91
|
-
auth,
|
|
92
95
|
source: () => {
|
|
93
96
|
allowEndpoint(sourceEndpoint, policy);
|
|
94
97
|
return pool.clientFor({ transport: 'http', url: sourceEndpoint });
|
|
95
98
|
},
|
|
99
|
+
...(auth === undefined ? {} : { auth }),
|
|
96
100
|
fetch: fetchImpl,
|
|
97
101
|
policy,
|
|
98
102
|
limits: { maximumPublicationBytes, maximumBundleBytes },
|
|
99
103
|
maximumResponseBytes,
|
|
100
|
-
|
|
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);
|
|
101
110
|
this.dispatcher = new SessionDispatcher({
|
|
102
111
|
kernel: this.kernel,
|
|
103
112
|
sourceEndpoint,
|
|
104
113
|
pool,
|
|
105
114
|
websocketAvailable,
|
|
106
115
|
auth,
|
|
116
|
+
...(this.callExchange === undefined ? {} : { exchange: this.callExchange }),
|
|
107
117
|
cache: this.routes,
|
|
108
118
|
policy,
|
|
109
119
|
envelopeTransport: admitEnvelopeTransport(options.envelopeTransport ?? 'http'),
|
|
@@ -127,13 +137,22 @@ export class ClientSession {
|
|
|
127
137
|
this.graph = createGraph((call, options) => this.call(call, options));
|
|
128
138
|
this.auth = createAuth((target, input, options) => this.call({ target, input }, options), (call, options) => {
|
|
129
139
|
const operation = this.operation(admitControlOptions(options));
|
|
130
|
-
return admitFunction(
|
|
140
|
+
return admitFunction(this.exchangeDependencies, acceptCall(call), options, operation);
|
|
131
141
|
});
|
|
132
142
|
}
|
|
133
143
|
/** Bind one admitted DSL Domain to authenticated graph projection and Method dispatch. */
|
|
134
144
|
bind(domain) {
|
|
135
145
|
return createDomainBinding(domain, this.graph, (node, method, input) => this.invokeDomainMethod(domain, node, method, input));
|
|
136
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
|
+
}
|
|
137
156
|
/** Revalidate the intrinsic Publication without acquiring its bundle or Views. */
|
|
138
157
|
publication(options) {
|
|
139
158
|
return this.discovery.publication(this.operation(admitControlOptions(options)));
|
|
@@ -208,6 +227,7 @@ export class ClientSession {
|
|
|
208
227
|
this.ownedPool?.close();
|
|
209
228
|
this.routes.clear();
|
|
210
229
|
this.discovery.clear();
|
|
230
|
+
this.callExchange?.clear();
|
|
211
231
|
}
|
|
212
232
|
requireOpen() {
|
|
213
233
|
if (this.closed)
|