@astrale-os/kernel-client 0.6.0-beta.80 → 0.6.0-beta.82
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/session/context/index.d.ts +1 -1
- package/dist/session/context/index.js +1 -1
- package/dist/session/exchange/options.d.ts +8 -3
- package/dist/session/exchange/options.js +10 -3
- package/dist/session/exchange/presentation.d.ts +1 -1
- package/dist/session/routing/cache.d.ts +1 -1
- package/dist/session/routing/cache.js +18 -2
- package/dist/session/routing/dispatch.js +6 -4
- package/dist/session/session.d.ts +4 -2
- package/dist/session/session.js +4 -3
- package/dist/session/session.typecheck.js +9 -0
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { admitControlOptions, type SessionControlOptions } from './control.js';
|
|
2
2
|
export { DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_MAXIMUM_ROUTES, DEFAULT_TIMEOUT_MS, platformDuration, positive, } from './limits.js';
|
|
3
|
-
export { createSessionOperation, runSessionOperation, sessionTransportOptions, type SessionOperation, } from './operation.js';
|
|
3
|
+
export { createSessionOperation, remainingSessionTime, runSessionOperation, sessionTransportOptions, type SessionOperation, } from './operation.js';
|
|
4
4
|
export { admitKernel, admitPolicy, allowEndpoint, kernelInvocationEndpoint, type SessionPolicy, } from './policy.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { admitControlOptions } from './control.js';
|
|
2
2
|
export { DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_MAXIMUM_ROUTES, DEFAULT_TIMEOUT_MS, platformDuration, positive, } from './limits.js';
|
|
3
|
-
export { createSessionOperation, runSessionOperation, sessionTransportOptions, } from './operation.js';
|
|
3
|
+
export { createSessionOperation, remainingSessionTime, runSessionOperation, sessionTransportOptions, } from './operation.js';
|
|
4
4
|
export { admitKernel, admitPolicy, allowEndpoint, kernelInvocationEndpoint, } from './policy.js';
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { type IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
2
|
import type { SessionControlOptions } from '../context/index.js';
|
|
3
3
|
import type { SessionAuth } from '../routing/index.js';
|
|
4
|
-
/**
|
|
4
|
+
/** Choose how authenticated Calls to remote Domains present the Session's source authority. */
|
|
5
5
|
export interface SessionExchangeOptions {
|
|
6
|
+
/**
|
|
7
|
+
* `auto` exchanges through each callable's declaring Domain; `source` keeps the source credential.
|
|
8
|
+
* Defaults to `auto`, including legacy empty objects and options containing only `ttlSeconds`.
|
|
9
|
+
*/
|
|
10
|
+
readonly mode?: 'auto' | 'source';
|
|
6
11
|
/**
|
|
7
12
|
* Delegation lifetime requested for each Domain credential. The source credential's remaining
|
|
8
|
-
* lifetime and the Domain's own ceiling may shorten it.
|
|
13
|
+
* lifetime and the Domain's own ceiling may shorten it. Applies only in `auto` mode.
|
|
9
14
|
*/
|
|
10
15
|
readonly ttlSeconds?: number;
|
|
11
16
|
}
|
|
@@ -24,6 +29,6 @@ export interface ExchangeRequest {
|
|
|
24
29
|
* Admit exchange options against the Session auth they extend. Every Domain credential must still
|
|
25
30
|
* cover the Session delegation, so a request that could never cover it is rejected.
|
|
26
31
|
*/
|
|
27
|
-
export declare function admitExchangeOptions(input: SessionExchangeOptions | undefined, auth: SessionAuth | undefined): Readonly<Required<SessionExchangeOptions
|
|
32
|
+
export declare function admitExchangeOptions(input: SessionExchangeOptions | undefined, auth: SessionAuth | undefined): Readonly<Required<Pick<SessionExchangeOptions, 'ttlSeconds'>>> | undefined;
|
|
28
33
|
/** Admit an explicit exchange: the canonical absolute issuer URL of the named Domain and its lifetime. */
|
|
29
34
|
export declare function admitExchangeRequest(domain: string, input: SessionExchangeRequest | undefined): ExchangeRequest;
|
|
@@ -12,13 +12,20 @@ export function admitExchangeOptions(input, auth) {
|
|
|
12
12
|
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
13
13
|
throw new TypeError('Session exchange options are invalid.');
|
|
14
14
|
}
|
|
15
|
-
if (Reflect.ownKeys(input).some((key) => key !== 'ttlSeconds')) {
|
|
15
|
+
if (Reflect.ownKeys(input).some((key) => key !== 'mode' && key !== 'ttlSeconds')) {
|
|
16
16
|
throw new TypeError('Session exchange options contain unexpected fields.');
|
|
17
17
|
}
|
|
18
|
+
const mode = input.mode === undefined ? 'auto' : input.mode;
|
|
19
|
+
if (mode !== 'auto' && mode !== 'source') {
|
|
20
|
+
throw new TypeError('Session exchange mode is invalid.');
|
|
21
|
+
}
|
|
22
|
+
if (mode === 'source')
|
|
23
|
+
return undefined;
|
|
18
24
|
if (auth === undefined)
|
|
19
25
|
throw new TypeError('Session exchange requires Session auth.');
|
|
20
|
-
const
|
|
21
|
-
|
|
26
|
+
const coverageMs = delegationCoverageMs(auth.ttlSeconds);
|
|
27
|
+
const ttlSeconds = admitTtlSeconds(input.ttlSeconds ?? Math.max(DEFAULT_EXCHANGE_TTL_SECONDS, Math.floor(coverageMs / 1_000) + 1));
|
|
28
|
+
if (ttlSeconds * 1_000 <= coverageMs) {
|
|
22
29
|
throw new TypeError('Session exchange ttlSeconds must exceed the Session auth ttlSeconds.');
|
|
23
30
|
}
|
|
24
31
|
return Object.freeze({ ttlSeconds });
|
|
@@ -15,4 +15,4 @@ export interface SessionExchange extends AuthExchange {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function createSessionExchange(dependencies: ExchangeDependencies & {
|
|
17
17
|
readonly auth: SessionAuth;
|
|
18
|
-
}, options: Readonly<Required<SessionExchangeOptions
|
|
18
|
+
}, options: Readonly<Required<Pick<SessionExchangeOptions, 'ttlSeconds'>>>): SessionExchange;
|
|
@@ -27,7 +27,7 @@ export declare class RouteCache {
|
|
|
27
27
|
private readonly store?;
|
|
28
28
|
constructor(maximumAgeMs: number, maximumSize?: number, store?: SessionRouteStore);
|
|
29
29
|
key(source: string, call: invocation.Call, auth: EffectiveAuth): Promise<string>;
|
|
30
|
-
get(key: string, sourceCredential?: string, now?: number): Promise<LearnedRoute | undefined>;
|
|
30
|
+
get(key: string, sourceCredential?: string, now?: number, operationDeadline?: number): Promise<LearnedRoute | undefined>;
|
|
31
31
|
learn(key: string, route: invocation.Route, credential: string | undefined, sourceCredential?: string, now?: number): Promise<LearnedRoute>;
|
|
32
32
|
delete(key: string): void;
|
|
33
33
|
/** Clear this Session's memory without deleting caller-owned persistent state. */
|
|
@@ -19,12 +19,12 @@ export class RouteCache {
|
|
|
19
19
|
const delegate = auth.delegate === undefined ? 'anonymous' : await digestText(canonicalDelegate(auth.delegate));
|
|
20
20
|
return JSON.stringify([source, call.target, credential, delegate]);
|
|
21
21
|
}
|
|
22
|
-
async get(key, sourceCredential, now
|
|
22
|
+
async get(key, sourceCredential, now, operationDeadline) {
|
|
23
23
|
const cached = this.entries.get(key);
|
|
24
24
|
if (cached === undefined)
|
|
25
25
|
return undefined;
|
|
26
26
|
const token = this.keyTokens.get(key);
|
|
27
|
-
if (cached.learned.expiresAt <= now) {
|
|
27
|
+
if (cached.learned.expiresAt <= (now ?? Date.now())) {
|
|
28
28
|
this.invalidate(key);
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
@@ -38,6 +38,17 @@ export class RouteCache {
|
|
|
38
38
|
this.invalidate(key);
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
|
+
// HMAC verification can outlast route age. Check the exact-entry fence before live facts.
|
|
42
|
+
if (cached.learned.expiresAt <= (now ?? Date.now())) {
|
|
43
|
+
this.invalidate(key);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (operationDeadline !== undefined &&
|
|
48
|
+
cached.credentialRefreshBoundary !== undefined &&
|
|
49
|
+
cached.credentialRefreshBoundary <= operationDeadline) {
|
|
50
|
+
// This operation needs a fresh carrier; a shorter concurrent operation may still reuse it.
|
|
51
|
+
return undefined;
|
|
41
52
|
}
|
|
42
53
|
this.entries.delete(key);
|
|
43
54
|
this.entries.set(key, cached);
|
|
@@ -45,6 +56,7 @@ export class RouteCache {
|
|
|
45
56
|
}
|
|
46
57
|
async learn(key, route, credential, sourceCredential, now = Date.now()) {
|
|
47
58
|
const credentialExpiry = credential === undefined ? undefined : jwtExpiry(credential);
|
|
59
|
+
const credentialRefreshBoundary = credentialExpiry === undefined ? undefined : refreshBoundary(credentialExpiry);
|
|
48
60
|
const expiresAt = Math.min(now + this.maximumAgeMs, refreshBoundary(credentialExpiry));
|
|
49
61
|
const entry = Object.freeze({
|
|
50
62
|
route,
|
|
@@ -79,6 +91,7 @@ export class RouteCache {
|
|
|
79
91
|
}
|
|
80
92
|
this.entries.set(key, Object.freeze({
|
|
81
93
|
learned: entry,
|
|
94
|
+
...(credentialRefreshBoundary === undefined ? {} : { credentialRefreshBoundary }),
|
|
82
95
|
...(integrity === undefined ? {} : { sealed: entry, integrity }),
|
|
83
96
|
}));
|
|
84
97
|
this.persist();
|
|
@@ -210,6 +223,9 @@ function admitStoredEntry(input, now, maximumAgeMs) {
|
|
|
210
223
|
}),
|
|
211
224
|
sealed,
|
|
212
225
|
integrity: value.integrity,
|
|
226
|
+
...(credentialExpiry === undefined
|
|
227
|
+
? {}
|
|
228
|
+
: { credentialRefreshBoundary: refreshBoundary(credentialExpiry) }),
|
|
213
229
|
});
|
|
214
230
|
}
|
|
215
231
|
catch {
|
|
@@ -2,7 +2,7 @@ import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
|
2
2
|
import * as invocationProtocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
3
|
import { acceptCall, Client, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
|
|
4
4
|
import { ClientError, ResponseError } from '../../errors/index.js';
|
|
5
|
-
import { runSessionOperation, sessionTransportOptions, allowEndpoint } from '../context/index.js';
|
|
5
|
+
import { runSessionOperation, remainingSessionTime, sessionTransportOptions, allowEndpoint, } from '../context/index.js';
|
|
6
6
|
import { resolveAuth } from './auth.js';
|
|
7
7
|
import { selectEnvelope, selectEnvelopeTarget } from './envelope.js';
|
|
8
8
|
export class SessionDispatcher {
|
|
@@ -27,14 +27,14 @@ export class SessionDispatcher {
|
|
|
27
27
|
? resolved
|
|
28
28
|
: await this.state.exchange.resolve(call, resolved, operation);
|
|
29
29
|
const source = this.state.kernel;
|
|
30
|
-
const cacheKey = await this.state.cache.key(source, call, auth);
|
|
30
|
+
const cacheKey = await runSessionOperation(operation, () => this.state.cache.key(source, call, auth));
|
|
31
31
|
const dispatch = Object.freeze({
|
|
32
32
|
call,
|
|
33
33
|
auth,
|
|
34
34
|
source,
|
|
35
35
|
cacheKey,
|
|
36
36
|
});
|
|
37
|
-
let learned = await this.state.cache.get(cacheKey, auth.credential);
|
|
37
|
+
let learned = await runSessionOperation(operation, () => this.state.cache.get(cacheKey, auth.credential, undefined, operation.deadline));
|
|
38
38
|
if (learned !== undefined) {
|
|
39
39
|
try {
|
|
40
40
|
requireCachedRoute(learned, source, call.target, streaming === undefined ? this.state.envelopeTransport : 'http', this.state.websocketAvailable, this.state.policy);
|
|
@@ -52,6 +52,7 @@ export class SessionDispatcher {
|
|
|
52
52
|
}
|
|
53
53
|
async dispatchSource(state, routing, options, operation, streaming) {
|
|
54
54
|
this.state.requireOpen();
|
|
55
|
+
remainingSessionTime(operation);
|
|
55
56
|
allowEndpoint(this.state.sourceEndpoint, this.state.policy);
|
|
56
57
|
const client = streaming === undefined
|
|
57
58
|
? this.state.pool.clientFor(Object.freeze({ transport: 'http', url: this.state.sourceEndpoint }))
|
|
@@ -77,7 +78,7 @@ export class SessionDispatcher {
|
|
|
77
78
|
throw new ClientError('Redirect issuer differs from the session-pinned source Kernel identity.');
|
|
78
79
|
}
|
|
79
80
|
requireTargetPublication(redirect.route, state.call.target);
|
|
80
|
-
const learned = await this.state.cache.learn(state.cacheKey, redirect.route, redirect.credential, state.auth.credential);
|
|
81
|
+
const learned = await runSessionOperation(operation, () => this.state.cache.learn(state.cacheKey, redirect.route, redirect.credential, state.auth.credential));
|
|
81
82
|
return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace, streaming);
|
|
82
83
|
}
|
|
83
84
|
async dispatchDestinationWithRecovery(learned, state, options, operation, trace, streaming) {
|
|
@@ -107,6 +108,7 @@ export class SessionDispatcher {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
async dispatchDestination(learned, call, options, operation, source, trace, streaming) {
|
|
111
|
+
remainingSessionTime(operation);
|
|
110
112
|
if (learned.route.via.issuer !== source) {
|
|
111
113
|
throw new ClientError('Learned route issuer differs from the session-pinned source Kernel.');
|
|
112
114
|
}
|
|
@@ -29,7 +29,9 @@ export interface ClientSessionOptions {
|
|
|
29
29
|
/**
|
|
30
30
|
* Present each Domain callable with a credential its declaring Domain exchanges for the `auth`
|
|
31
31
|
* caller, so the Domain is the principal and the caller is `auth`. Kernel and local callables keep
|
|
32
|
-
* the `auth` credential.
|
|
32
|
+
* the `auth` credential. `connect` defaults to `auto` when `auth` is present; `new ClientSession`
|
|
33
|
+
* keeps source authority when this option is omitted. `{ mode: 'source' }` keeps it for every Call.
|
|
34
|
+
* Legacy `{}` and `{ ttlSeconds }` options remain equivalent to `{ mode: 'auto' }`.
|
|
33
35
|
*/
|
|
34
36
|
readonly exchange?: SessionExchangeOptions;
|
|
35
37
|
/** Optional caller-owned persistence for bounded confidential route artifacts. */
|
|
@@ -91,5 +93,5 @@ export declare class ClientSession {
|
|
|
91
93
|
private invokeDomainMethod;
|
|
92
94
|
private operation;
|
|
93
95
|
}
|
|
94
|
-
/** Open
|
|
96
|
+
/** Open a Kernel conversation with secure defaults and automatic Domain exchange when authenticated. */
|
|
95
97
|
export declare function connect(options: ConnectOptions): ClientSession;
|
package/dist/session/session.js
CHANGED
|
@@ -253,16 +253,17 @@ export class ClientSession {
|
|
|
253
253
|
return createSessionOperation(this.lifecycle.signal, options, this.timeoutMs);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
/** Open
|
|
256
|
+
/** Open a Kernel conversation with secure defaults and automatic Domain exchange when authenticated. */
|
|
257
257
|
export function connect(options) {
|
|
258
258
|
if (options === null || typeof options !== 'object') {
|
|
259
259
|
throw new TypeError('Connect options are invalid.');
|
|
260
260
|
}
|
|
261
|
-
const { url, ...rest } = options;
|
|
261
|
+
const { url, exchange, ...rest } = options;
|
|
262
262
|
return new ClientSession({
|
|
263
263
|
...rest,
|
|
264
264
|
kernel: url,
|
|
265
|
-
|
|
265
|
+
exchange: exchange === undefined && rest.auth !== undefined ? { mode: 'auto' } : exchange,
|
|
266
|
+
policy: rest.policy ?? DEFAULT_SESSION_POLICY,
|
|
266
267
|
});
|
|
267
268
|
}
|
|
268
269
|
function admitClientPool(input) {
|
|
@@ -28,6 +28,15 @@ const credentials = createSessionCredentialProvider({
|
|
|
28
28
|
// A credential provider is a Session authentication resolver; no adapter stands between them.
|
|
29
29
|
const credentialAuth = credentials;
|
|
30
30
|
void new ClientSession({ ...options, auth: credentialAuth });
|
|
31
|
+
// Automatic exchange remains compatible with the original empty and TTL-only options.
|
|
32
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: {} });
|
|
33
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: { ttlSeconds: 300 } });
|
|
34
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'auto' } });
|
|
35
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'source' } });
|
|
36
|
+
// @ts-expect-error Exchange accepts named modes, not a boolean switch.
|
|
37
|
+
void new ClientSession({ ...options, exchange: true });
|
|
38
|
+
// @ts-expect-error Unknown exchange modes are not admitted.
|
|
39
|
+
void new ClientSession({ ...options, exchange: { mode: 'invalid' } });
|
|
31
40
|
// @ts-expect-error A minted credential must carry its own absolute expiry.
|
|
32
41
|
void createSessionCredentialProvider({ ttlSeconds: 60, mint: async () => minted.credential });
|
|
33
42
|
const dynamicReference = reference(dynamicDomain, dynamicMethod)(Path.id(NodeId('dynamic-node')));
|
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.82",
|
|
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.
|
|
68
|
+
"@astrale-os/kernel-core": "0.9.0-beta.61",
|
|
69
69
|
"@astrale-os/kernel-dsl": "0.2.0-beta.43",
|
|
70
|
-
"@astrale-os/kernel-protocol": "0.5.0-beta.
|
|
70
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.67",
|
|
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.69",
|
|
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",
|