@astrale-os/kernel-client 0.6.0-beta.70 → 0.6.0-beta.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/auth/auth.d.ts +17 -5
  2. package/dist/auth/auth.js +3 -4
  3. package/dist/auth/credential.d.ts +5 -1
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.js +0 -1
  6. package/dist/client/client.d.ts +4 -2
  7. package/dist/client/client.js +13 -2
  8. package/dist/client/credential.d.ts +4 -2
  9. package/dist/client/credential.js +7 -2
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/client/index.js +1 -1
  12. package/dist/client/request.d.ts +7 -1
  13. package/dist/client/request.js +31 -6
  14. package/dist/index.d.ts +1 -1
  15. package/dist/session/admission.d.ts +18 -0
  16. package/dist/session/admission.js +117 -0
  17. package/dist/session/bound.d.ts +12 -9
  18. package/dist/session/bound.js +24 -5
  19. package/dist/session/content.d.ts +30 -0
  20. package/dist/session/content.js +160 -0
  21. package/dist/session/context/operation.d.ts +5 -1
  22. package/dist/session/discovery/index.d.ts +1 -0
  23. package/dist/session/discovery/index.js +1 -0
  24. package/dist/session/index.d.ts +3 -2
  25. package/dist/session/reference.d.ts +7 -3
  26. package/dist/session/reference.js +1 -1
  27. package/dist/session/routing/dispatch.d.ts +10 -0
  28. package/dist/session/routing/dispatch.js +68 -23
  29. package/dist/session/routing/index.d.ts +1 -1
  30. package/dist/session/routing/index.js +1 -1
  31. package/dist/session/routing/request.d.ts +9 -0
  32. package/dist/session/routing/request.js +17 -0
  33. package/dist/session/session.d.ts +12 -3
  34. package/dist/session/session.js +58 -13
  35. package/dist/session/session.typecheck.js +46 -1
  36. package/dist/transport/http/byte-stream.d.ts +2 -1
  37. package/dist/transport/http/byte-stream.js +9 -1
  38. package/dist/transport/http/transport.d.ts +3 -0
  39. package/dist/transport/http/transport.js +42 -10
  40. package/package.json +6 -6
@@ -1,13 +1,25 @@
1
- import type { Identity, Authentication, IdentityId, MintedCredential, RegisterResult } from '@astrale-os/kernel-core/auth';
2
- import type { PathLike } from '@astrale-os/kernel-core/path';
3
- import type { Input } from '../client/index.js';
1
+ import type { Identity, Authentication, IdentityId, MintedCredential, RegisterResult, UnresolvedIdentityExpr } from '@astrale-os/kernel-core/auth';
2
+ import type { NodePath, PathLike } from '@astrale-os/kernel-core/path';
3
+ import type { Key } from '@astrale-os/kernel-dsl/v1/addressing';
4
+ import type { Call, Input } from '../client/index.js';
4
5
  import type { RequestOptions } from '../transport/index.js';
5
6
  import type { PolicyDecisionInput } from './authorization.js';
6
7
  import type { DelegateOptions, MintOptions, ImpersonateOptions } from './credential.js';
7
8
  import type { WhoisInput } from './identity.js';
8
9
  import type { RegisterRequest } from './register.js';
9
10
  export type AuthCall = (target: PathLike, input: Input, options?: RequestOptions) => Promise<Input>;
11
+ /** Current admission of one remote Call; the target has not been executed. */
12
+ export interface FunctionAdmission {
13
+ readonly callable: Key.Callable;
14
+ readonly receiver?: NodePath;
15
+ }
16
+ export interface AdmissionOptions extends RequestOptions {
17
+ /** Intersect the caller's Grant; defaults to explicit session attenuation, then self. */
18
+ readonly attenuation?: UnresolvedIdentityExpr;
19
+ }
10
20
  export interface AuthApi {
21
+ /** Check access and input without executing an installed remote callable. */
22
+ admit(call: Call, options?: AdmissionOptions): Promise<FunctionAdmission>;
11
23
  register(input: RegisterRequest, options?: RequestOptions): Promise<RegisterResult>;
12
24
  whoami(options?: RequestOptions): Promise<Identity & Authentication>;
13
25
  whois(input: WhoisInput, options?: RequestOptions): Promise<(Identity & Authentication) | null>;
@@ -18,5 +30,5 @@ export interface AuthApi {
18
30
  mint(input: MintOptions, options?: RequestOptions): Promise<MintedCredential>;
19
31
  delegate(identity: IdentityId, input: DelegateOptions, options?: RequestOptions): Promise<MintedCredential>;
20
32
  }
21
- /** Bind the current Core identity syscalls without recreating legacy grant-edge verbs. */
22
- export declare function createAuth(call: AuthCall): AuthApi;
33
+ /** Internal composition over callbacks owned by the Session or its bound view. */
34
+ export declare function createAuth(call: AuthCall, admit: AuthApi['admit']): AuthApi;
package/dist/auth/auth.js CHANGED
@@ -2,11 +2,10 @@ import { authorizationApi } from './authorization.js';
2
2
  import { credentialApi } from './credential.js';
3
3
  import { identityApi } from './identity.js';
4
4
  import { registerApi } from './register.js';
5
- /** Bind the current Core identity syscalls without recreating legacy grant-edge verbs. */
6
- export function createAuth(call) {
7
- if (typeof call !== 'function')
8
- throw new TypeError('Auth call capability is invalid.');
5
+ /** Internal composition over callbacks owned by the Session or its bound view. */
6
+ export function createAuth(call, admit) {
9
7
  return Object.freeze({
8
+ admit,
10
9
  ...registerApi(call),
11
10
  ...identityApi(call),
12
11
  ...authorizationApi(call),
@@ -13,7 +13,11 @@ export interface ImpersonateOptions extends MintOptions {
13
13
  export interface DelegateOptions {
14
14
  readonly audience: string;
15
15
  readonly ttlSeconds: number;
16
- readonly attenuation: UnresolvedIdentityExpr;
16
+ /**
17
+ * Sent only when present; the Kernel reads absence as self. A Kernel whose `Identity.delegate`
18
+ * still requires it rejects the omission.
19
+ */
20
+ readonly attenuation?: UnresolvedIdentityExpr;
17
21
  }
18
22
  export declare const IDENTITY_IMPERSONATE_PATH: NodePath;
19
23
  export declare const IDENTITY_MINT_PATH: NodePath;
@@ -1,4 +1,4 @@
1
- export { createAuth, type AuthApi, type AuthCall } from './auth.js';
1
+ export type { AuthApi, AdmissionOptions, FunctionAdmission } from './auth.js';
2
2
  export { IDENTITY_CAN_PATH, type PolicyDecisionInput } from './authorization.js';
3
3
  export { IDENTITY_MINT_PATH, IDENTITY_IMPERSONATE_PATH, type DelegateOptions, type MintOptions, type ImpersonateOptions, } from './credential.js';
4
4
  export { IDENTITY_WHOAMI_PATH, IDENTITY_WHOIS_PATH, type WhoisInput } from './identity.js';
@@ -1,4 +1,3 @@
1
- export { createAuth } from './auth.js';
2
1
  export { IDENTITY_CAN_PATH } from './authorization.js';
3
2
  export { IDENTITY_MINT_PATH, IDENTITY_IMPERSONATE_PATH, } from './credential.js';
4
3
  export { IDENTITY_WHOAMI_PATH, IDENTITY_WHOIS_PATH } from './identity.js';
@@ -1,14 +1,14 @@
1
1
  import type { invocation as protocol } from '@astrale-os/kernel-protocol';
2
2
  import type { Transport } from '../transport/index.js';
3
3
  import type { AuthenticatedClient, RemoteCredential } from './credential.js';
4
- import type { Call, ClientDispatchOptions } from './request.js';
4
+ import type { Call, ClientDispatchOptions, ClientDispatchOptionsWithBody } from './request.js';
5
5
  import type { BinaryResponse, Result, StreamResult, ValueResult } from './result.js';
6
6
  import { type HttpTransportOptions } from '../transport/http/index.js';
7
7
  export type ClientOptions = HttpTransportOptions | {
8
8
  readonly transport: Transport;
9
9
  };
10
10
  /** Package-private redirect continuation path; intentionally absent from the public Client facade. */
11
- export declare function dispatchTrace(client: Client, credential: RemoteCredential | undefined, call: Call, options: ClientDispatchOptions | undefined, trace: protocol.Trace): Promise<Result>;
11
+ export declare function dispatchTrace(client: Client, credential: RemoteCredential | undefined, call: Call, options: ClientDispatchOptions | ClientDispatchOptionsWithBody | undefined, trace: protocol.Trace): Promise<Result>;
12
12
  /** Pure client for the universal remote Function invocation boundary. */
13
13
  export declare class Client implements AuthenticatedClient {
14
14
  private readonly transport;
@@ -16,7 +16,9 @@ export declare class Client implements AuthenticatedClient {
16
16
  private closed;
17
17
  constructor(options: ClientOptions);
18
18
  dispatch(call: Call, options?: ClientDispatchOptions): Promise<Result>;
19
+ dispatch(call: Call, options: ClientDispatchOptionsWithBody): Promise<Result>;
19
20
  call(call: Call, options?: ClientDispatchOptions): Promise<ValueResult>;
21
+ call(call: Call, options: ClientDispatchOptionsWithBody): Promise<ValueResult>;
20
22
  stream(call: Call, options?: ClientDispatchOptions): Promise<StreamResult>;
21
23
  binary(call: Call, options?: ClientDispatchOptions): Promise<BinaryResponse>;
22
24
  as(credential: RemoteCredential): AuthenticatedClient;
@@ -30,7 +30,7 @@ export class Client {
30
30
  return this.dispatchWith(undefined, call, options);
31
31
  }
32
32
  call(call, options) {
33
- return requireValue(this.dispatch(call, options));
33
+ return requireValue(this.dispatchWith(undefined, call, options));
34
34
  }
35
35
  stream(call, options) {
36
36
  return requireStream(this.dispatch(call, options));
@@ -57,7 +57,18 @@ export class Client {
57
57
  ? undefined
58
58
  : await resolveCredential(credential, request.options.signal);
59
59
  requireRequestLive(request.options.signal);
60
- const result = await this.transport.dispatch(invocation(request, resolved, trace), request.options);
60
+ const envelope = invocation(request, resolved, trace);
61
+ let result;
62
+ if (request.body === undefined) {
63
+ result = await this.transport.dispatch(envelope, request.options);
64
+ }
65
+ else {
66
+ const transport = this.transport;
67
+ if (!(transport instanceof HttpTransport)) {
68
+ throw new TypeError('Binary dispatch requires HTTP transport.');
69
+ }
70
+ result = await transport.dispatchBinary(envelope, request.body, request.options);
71
+ }
61
72
  if (result.kind === 'stale')
62
73
  throw admittedStaleRouteError(result.current, result.invocation);
63
74
  if (result.kind === 'miss')
@@ -1,12 +1,14 @@
1
- import type { Call, ClientDispatchOptions } from './request.js';
1
+ import type { Call, ClientDispatchOptions, ClientDispatchOptionsWithBody } from './request.js';
2
2
  import type { BinaryResponse, Result, StreamResult, ValueResult } from './result.js';
3
3
  export type RemoteCredential = string | (() => string | Promise<string>);
4
4
  export interface AuthenticatedClient {
5
5
  dispatch(call: Call, options?: ClientDispatchOptions): Promise<Result>;
6
+ dispatch(call: Call, options: ClientDispatchOptionsWithBody): Promise<Result>;
6
7
  call(call: Call, options?: ClientDispatchOptions): Promise<ValueResult>;
8
+ call(call: Call, options: ClientDispatchOptionsWithBody): Promise<ValueResult>;
7
9
  stream(call: Call, options?: ClientDispatchOptions): Promise<StreamResult>;
8
10
  binary(call: Call, options?: ClientDispatchOptions): Promise<BinaryResponse>;
9
11
  }
10
- export type CredentialDispatch = (credential: RemoteCredential, call: Call, options?: ClientDispatchOptions) => Promise<Result>;
12
+ export type CredentialDispatch = (credential: RemoteCredential, call: Call, options?: ClientDispatchOptions | ClientDispatchOptionsWithBody) => Promise<Result>;
11
13
  export declare function authenticated(dispatchWith: CredentialDispatch, credential: RemoteCredential): AuthenticatedClient;
12
14
  export declare function resolveCredential(input: RemoteCredential, signal?: AbortSignal): Promise<string>;
@@ -1,10 +1,15 @@
1
1
  import { ClientError } from '../errors/index.js';
2
2
  import { requireBinary, requireStream, requireValue } from './result.js';
3
3
  export function authenticated(dispatchWith, credential) {
4
- const dispatch = (call, options) => dispatchWith(credential, call, options);
4
+ function dispatch(call, options) {
5
+ return dispatchWith(credential, call, options);
6
+ }
7
+ function call(request, options) {
8
+ return requireValue(dispatchWith(credential, request, options));
9
+ }
5
10
  return Object.freeze({
6
11
  dispatch,
7
- call: (call, options) => requireValue(dispatch(call, options)),
12
+ call,
8
13
  stream: (call, options) => requireStream(dispatch(call, options)),
9
14
  binary: (call, options) => requireBinary(dispatch(call, options)),
10
15
  });
@@ -1,6 +1,6 @@
1
1
  export { Client, dispatchTrace, type ClientOptions } from './client.js';
2
2
  export { acceptCall } from './admission.js';
3
3
  export type { AuthenticatedClient, RemoteCredential } from './credential.js';
4
- export { call, type Call, type ClientDispatchOptions } from './request.js';
4
+ export { call, type Call, type ClientDispatchOptions, type ClientDispatchOptionsWithBody, } from './request.js';
5
5
  export type { Binary, BinaryResponse, Input, RedirectResult, Result, Stream, StreamResult, ValueResult, } from './result.js';
6
6
  export { isAdmittedMissError, isAdmittedStaleRouteError } from './routing-control.js';
@@ -1,4 +1,4 @@
1
1
  export { Client, dispatchTrace } from './client.js';
2
2
  export { acceptCall } from './admission.js';
3
- export { call } from './request.js';
3
+ export { call, } from './request.js';
4
4
  export { isAdmittedMissError, isAdmittedStaleRouteError } from './routing-control.js';
@@ -9,19 +9,25 @@ export interface Call {
9
9
  readonly input: Input;
10
10
  }
11
11
  export interface ClientDispatchOptions extends RequestOptions {
12
+ readonly body?: never;
12
13
  /** Untrusted delivery context; it changes neither Call meaning nor authority. */
13
14
  readonly delegate?: protocol.Delegate;
14
15
  readonly routing?: protocol.Routing;
15
16
  /** Package-owned local-target or remote-revision precondition for a bound invocation. */
16
17
  readonly schema?: protocol.SchemaTarget;
17
18
  }
19
+ export type ClientDispatchOptionsWithBody = Omit<ClientDispatchOptions, 'body' | 'idempotencyKey'> & {
20
+ readonly body: Blob;
21
+ readonly idempotencyKey?: never;
22
+ };
18
23
  export interface AdmittedRequest {
19
24
  readonly call: protocol.Call;
20
25
  readonly delegate?: protocol.Delegate;
21
26
  readonly routing?: protocol.Routing;
22
27
  readonly schema?: protocol.SchemaTarget;
23
28
  readonly options: RequestOptions;
29
+ readonly body?: Blob;
24
30
  }
25
31
  export declare function call(target: PathLike | Target, input: Input): Call;
26
- export declare function admitRequest(input: Call, options: ClientDispatchOptions | undefined, lifecycle: AbortSignal): AdmittedRequest;
32
+ export declare function admitRequest(input: Call, options: ClientDispatchOptions | ClientDispatchOptionsWithBody | undefined, lifecycle: AbortSignal): AdmittedRequest;
27
33
  export declare function invocation(request: AdmittedRequest, credential?: string, trace?: protocol.Trace): TransportRequest;
@@ -5,7 +5,8 @@ export function call(target, input) {
5
5
  }
6
6
  export function admitRequest(input, options, lifecycle) {
7
7
  const call = acceptCall(input);
8
- const admittedOptions = lifecycleOptions(admitRequestOptions(options), lifecycle);
8
+ const admitted = admitRequestOptions(options);
9
+ const admittedOptions = lifecycleOptions(admitted.options, lifecycle);
9
10
  const delegate = admitDelegate(options?.delegate);
10
11
  const routing = admitRouting(options?.routing);
11
12
  const schema = admitSchemaTarget(options?.schema);
@@ -14,6 +15,7 @@ export function admitRequest(input, options, lifecycle) {
14
15
  ...(delegate === undefined ? {} : { delegate }),
15
16
  ...(routing === undefined ? {} : { routing }),
16
17
  ...(schema === undefined ? {} : { schema }),
18
+ ...(admitted.body === undefined ? {} : { body: admitted.body }),
17
19
  options: admittedOptions,
18
20
  });
19
21
  }
@@ -41,11 +43,19 @@ export function invocation(request, credential, trace) {
41
43
  }
42
44
  function admitRequestOptions(input) {
43
45
  if (input === undefined)
44
- return undefined;
46
+ return {};
45
47
  if (input === null || typeof input !== 'object' || Array.isArray(input)) {
46
48
  throw new TypeError('Client request options must be an object.');
47
49
  }
48
- if (Reflect.ownKeys(input).some((field) => !['signal', 'timeoutMs', 'idempotencyKey', 'delegate', 'routing', 'schema'].includes(String(field)))) {
50
+ if (Reflect.ownKeys(input).some((field) => ![
51
+ 'signal',
52
+ 'timeoutMs',
53
+ 'idempotencyKey',
54
+ 'delegate',
55
+ 'routing',
56
+ 'schema',
57
+ 'body',
58
+ ].includes(String(field)))) {
49
59
  throw new TypeError('Client request options contain unsupported fields.');
50
60
  }
51
61
  if (input.timeoutMs !== undefined &&
@@ -54,13 +64,28 @@ function admitRequestOptions(input) {
54
64
  input.timeoutMs > 2_147_483_647)) {
55
65
  throw new TypeError('Client request timeoutMs must be a positive safe platform integer.');
56
66
  }
67
+ const hasBody = Object.hasOwn(input, 'body');
68
+ let body;
69
+ if (hasBody) {
70
+ const candidate = input.body;
71
+ if (!(candidate instanceof Blob)) {
72
+ throw new TypeError('Binary input must be a Blob or File.');
73
+ }
74
+ body = candidate;
75
+ }
76
+ if (body !== undefined && input.idempotencyKey !== undefined) {
77
+ throw new TypeError('Binary requests cannot carry an idempotency key.');
78
+ }
57
79
  const idempotencyKey = input.idempotencyKey === undefined
58
80
  ? undefined
59
81
  : invocationProtocol.acceptIdempotencyKey(input.idempotencyKey, 'request.idempotencyKey');
60
82
  return Object.freeze({
61
- ...(input.signal === undefined ? {} : { signal: input.signal }),
62
- ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
63
- ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
83
+ options: Object.freeze({
84
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
85
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
86
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
87
+ }),
88
+ ...(body === undefined ? {} : { body }),
64
89
  });
65
90
  }
66
91
  function admitDelegate(input) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Target, targetPath } from '@astrale-os/kernel-core/callable';
2
- export { Client, call, type AuthenticatedClient, type Call, type ClientDispatchOptions, type ClientOptions, type Binary, type BinaryResponse, type Input, type RemoteCredential, type Result, type Stream, type StreamResult, type ValueResult, } from './client/index.js';
2
+ export { Client, call, type AuthenticatedClient, type Call, type ClientDispatchOptions, type ClientDispatchOptionsWithBody, type ClientOptions, type Binary, type BinaryResponse, type Input, type RemoteCredential, type Result, type Stream, type StreamResult, type ValueResult, } from './client/index.js';
3
3
  export * from './errors/index.js';
4
4
  export { type RequestOptions, type MissControl, type StaleControl, type Transport, type TransportRequest, type TransportResult, } from './transport/index.js';
5
5
  export { HttpTransport, type Fetch, type HttpTransportOptions } from './transport/http/index.js';
@@ -0,0 +1,18 @@
1
+ import type { invocation } from '@astrale-os/kernel-protocol';
2
+ import type { AdmissionOptions, FunctionAdmission } from '../auth/index.js';
3
+ import type { Client } from '../client/index.js';
4
+ import type { SchemaOptions } from '../schema/schema.js';
5
+ import type { Fetch } from '../transport/http/index.js';
6
+ import type { SessionOperation, SessionPolicy } from './context/index.js';
7
+ import type { SessionAuth } from './routing/index.js';
8
+ export interface FunctionAdmissionDependencies {
9
+ readonly kernel: string;
10
+ readonly auth?: SessionAuth;
11
+ readonly source: () => Client;
12
+ readonly fetch: Fetch;
13
+ readonly policy: Readonly<SessionPolicy>;
14
+ readonly limits: SchemaOptions;
15
+ readonly maximumResponseBytes: number;
16
+ }
17
+ /** Check a remote Call through the existing Domain exchange and Kernel admission protocols. */
18
+ export declare function admitFunction(dependencies: FunctionAdmissionDependencies, call: invocation.Call, options: AdmissionOptions | undefined, operation: SessionOperation): Promise<FunctionAdmission>;
@@ -0,0 +1,117 @@
1
+ import { grant } from '@astrale-os/kernel-core/auth';
2
+ import { targetPath } from '@astrale-os/kernel-core/callable';
3
+ import { Path } from '@astrale-os/kernel-core/path';
4
+ import { admitOutput } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
5
+ import { function as functions } from '@astrale-os/kernel-core/schema/kernel/syscalls/references';
6
+ import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
7
+ import { codecs, issuer } from '@astrale-os/kernel-protocol';
8
+ import { credentialApi } from '../auth/credential.js';
9
+ import { identityApi } from '../auth/identity.js';
10
+ import { ClientError } from '../errors/index.js';
11
+ import { createSchema } from '../schema/index.js';
12
+ import { allowEndpoint, runSessionOperation, sessionTransportOptions } from './context/index.js';
13
+ import { boundedResponse, requireExactResponse } from './discovery/index.js';
14
+ import { matchesCallable } from './reference.js';
15
+ import { requireResultKind, resolveAuth } from './routing/index.js';
16
+ const ADMIT = Path.staticMethod(Path.project(functions.admit.owner), functions.admit.name);
17
+ /** Check a remote Call through the existing Domain exchange and Kernel admission protocols. */
18
+ export async function admitFunction(dependencies, call, options, operation) {
19
+ const target = Path.parse(targetPath(call.target));
20
+ const origin = callableOrigin(target);
21
+ const attenuation = options?.attenuation === undefined
22
+ ? undefined
23
+ : grant.acceptUnresolved({ v: 1, expr: options.attenuation }).expr;
24
+ const effective = await resolveAuth(dependencies.auth, call, operation);
25
+ if (effective.credential === undefined || effective.delegate === undefined) {
26
+ throw new ClientError('Function admission requires an authenticated session.');
27
+ }
28
+ // Resolve once for the intended Call, then retain that exact caller across every helper syscall.
29
+ const client = dependencies.source();
30
+ const caller = client.as(effective.credential);
31
+ const dispatch = (input) => runSessionOperation(operation, () => caller.dispatch(input, {
32
+ ...sessionTransportOptions(operation),
33
+ delegate: effective.delegate,
34
+ }));
35
+ const callIdentity = async (target, input) => requireResultKind('value', await dispatch({ target, input })).value;
36
+ const identity = identityApi(callIdentity);
37
+ const credentials = credentialApi(callIdentity);
38
+ const schema = createSchema(dispatch, dependencies.limits);
39
+ const [domain, self] = await Promise.all([schema.inspect(origin), identity.whoami()]);
40
+ const remote = domain.publication;
41
+ if (remote === null || remote.identity.issuer === dependencies.kernel) {
42
+ throw new ClientError('Function admission through token exchange requires a remote Domain.');
43
+ }
44
+ const configuration = issuer.acceptConfiguration(await fetchJson(dependencies, new URL(issuer.paths(remote.identity.issuer).configuration, remote.identity.issuer).href, operation), remote.identity.issuer);
45
+ const endpoint = configuration.token_exchange_endpoint;
46
+ if (endpoint === undefined) {
47
+ throw new ClientError('The installed Domain does not advertise token exchange.');
48
+ }
49
+ allowEndpoint(endpoint, dependencies.policy);
50
+ const credential = await credentials.delegate(self.id, {
51
+ audience: remote.identity.issuer,
52
+ ttlSeconds: effective.delegate.ttlSeconds,
53
+ attenuation: attenuation ?? effective.delegate.attenuation ?? { kind: 'identity', self: true },
54
+ });
55
+ const exchanged = issuer.exchange.acceptResponse(await fetchJson(dependencies, endpoint, operation, credential));
56
+ if (exchanged.expiresAt * 1_000 <= Date.now()) {
57
+ throw new ClientError('Domain token exchange returned an expired credential.');
58
+ }
59
+ const result = await runSessionOperation(operation, () => client
60
+ .as(exchanged.token)
61
+ .dispatch({ target: ADMIT, input: { call, credential } }, { ...sessionTransportOptions(operation), delegate: effective.delegate }));
62
+ const output = admitOutput.parse(requireResultKind('value', result).value);
63
+ const callable = Key(output.callable);
64
+ if (!matchesCallable(target, callable)) {
65
+ throw new ClientError('Function admission returned a different callable.');
66
+ }
67
+ const receiver = output.receiver === undefined ? undefined : Path.parse(output.receiver);
68
+ const method = target.ast.steps.at(-1);
69
+ const instance = method?.kind === 'method' && method.dispatch === 'instance';
70
+ if (instance !== (receiver !== undefined) ||
71
+ (receiver !== undefined &&
72
+ (receiver.ast.anchor.kind !== 'id' ||
73
+ receiver.ast.steps.length !== 0 ||
74
+ (target.ast.anchor.kind === 'id' &&
75
+ target.ast.steps.length === 1 &&
76
+ receiver.ast.anchor.id !== target.ast.anchor.id)))) {
77
+ throw new ClientError('Function admission returned a different receiver.');
78
+ }
79
+ return Object.freeze({ callable, ...(receiver === undefined ? {} : { receiver: receiver.raw }) });
80
+ }
81
+ function callableOrigin(path) {
82
+ const last = path.ast.steps.at(-1);
83
+ if (last?.kind === 'method' && last.dispatch === 'instance')
84
+ return Key.origin(last.class);
85
+ if (path.ast.anchor.kind === 'domain')
86
+ return path.ast.anchor.origin;
87
+ throw new ClientError('Function admission requires a Domain-qualified callable target.');
88
+ }
89
+ async function fetchJson(dependencies, url, operation, credential) {
90
+ allowEndpoint(url, dependencies.policy);
91
+ return runSessionOperation(operation, async (signal) => {
92
+ const init = {
93
+ method: credential === undefined ? 'GET' : 'POST',
94
+ headers: {
95
+ accept: 'application/json',
96
+ ...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
97
+ },
98
+ credentials: 'omit',
99
+ cache: 'no-store',
100
+ referrerPolicy: 'no-referrer',
101
+ redirect: 'manual',
102
+ signal,
103
+ };
104
+ const response = await dependencies.fetch(url, init);
105
+ try {
106
+ requireExactResponse(response, url, 'Domain token exchange');
107
+ if (!response.ok) {
108
+ throw new ClientError(`Domain ${credential === undefined ? 'discovery' : 'token exchange'} returned HTTP ${response.status}.`);
109
+ }
110
+ return codecs.json.decode(await boundedResponse(response, dependencies.maximumResponseBytes, signal));
111
+ }
112
+ finally {
113
+ // Also release unread bodies rejected by URL, status, or declared-size validation.
114
+ void response.body?.cancel().catch(() => undefined);
115
+ }
116
+ });
117
+ }
@@ -1,20 +1,22 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth';
2
+ import type { AuthApi } from '../auth/index.js';
2
3
  import type { SchemaApi } from '../schema/index.js';
4
+ import type { ContentApi } from './content.js';
3
5
  import type { SessionControlOptions } from './context/index.js';
4
- import type { CallableReference, InputOfReference, OutputOfReference } from './reference.js';
5
6
  import type { SessionRequestOptions } from './routing/index.js';
6
7
  import type { ClientSession } from './session.js';
7
8
  import type { ViewCatalog } from './views/index.js';
8
- import { type AuthApi } from '../auth/index.js';
9
- import { type Binary, type Call, type Input, type Result, type Stream } from '../client/index.js';
9
+ import { type Binary, type Call, type Stream } from '../client/index.js';
10
10
  import { type GraphApi } from '../graph/index.js';
11
11
  /** Exact live Session surface captured by one credential-bound Kernel view. */
12
12
  export interface BoundSessionSource {
13
13
  readonly kernel: IssuerId;
14
14
  readonly schema: SchemaApi;
15
- dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
16
- call(call: Call, options?: SessionRequestOptions): Promise<Input>;
17
- invoke<const Reference extends CallableReference<unknown>>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
15
+ readonly auth: Pick<AuthApi, 'admit'>;
16
+ readonly content?: ContentApi;
17
+ dispatch: ClientSession['dispatch'];
18
+ call: ClientSession['call'];
19
+ invoke: ClientSession['invoke'];
18
20
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
19
21
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
20
22
  viewsFor(target: Parameters<ClientSession['viewsFor']>[0], options?: SessionControlOptions): Promise<ViewCatalog>;
@@ -34,9 +36,10 @@ export interface BoundClientSession extends GraphApi {
34
36
  readonly kernel: IssuerId;
35
37
  readonly auth: AuthApi;
36
38
  readonly schema: SchemaApi;
37
- dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
38
- call(call: Call, options?: SessionRequestOptions): Promise<Input>;
39
- invoke<const Reference extends CallableReference<unknown>>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
39
+ readonly content: ContentApi;
40
+ dispatch: ClientSession['dispatch'];
41
+ call: ClientSession['call'];
42
+ invoke: ClientSession['invoke'];
40
43
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
41
44
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
42
45
  viewsFor: BoundSessionSource['viewsFor'];
@@ -1,4 +1,4 @@
1
- import { createAuth } from '../auth/index.js';
1
+ import { createAuth } from '../auth/auth.js';
2
2
  import { call as createCall, } from '../client/index.js';
3
3
  import { createGraph } from '../graph/index.js';
4
4
  /** Capture one Session into the ordinary graph, auth, and invocation Kernel journey. */
@@ -7,17 +7,27 @@ export function bindSession(source, controls) {
7
7
  invalid();
8
8
  const admittedControls = admitControls(controls);
9
9
  const dispatchSource = capture(source, 'dispatch');
10
+ const admitSource = capture(source.auth, 'admit');
10
11
  const callSource = capture(source, 'call');
11
12
  const invokeSource = capture(source, 'invoke');
12
13
  const streamSource = capture(source, 'stream');
13
14
  const binarySource = capture(source, 'binary');
15
+ const contentSource = source.content === undefined ? unavailableContent : source.content;
16
+ if (contentSource === null || typeof contentSource !== 'object')
17
+ invalid();
18
+ const uploadSource = capture(contentSource, 'upload');
19
+ const downloadSource = capture(contentSource, 'download');
14
20
  const viewsForSource = capture(source, 'viewsFor');
15
21
  const schemaSource = captureSchema(source.schema);
16
- const dispatch = (input, options) => dispatchSource(input, requestOptions(options, admittedControls));
17
- const call = (input, options) => callSource(input, requestOptions(options, admittedControls));
18
- const invoke = (reference, input, options) => invokeSource(reference, input, requestOptions(options, admittedControls));
22
+ const dispatch = ((input, options) => dispatchSource(input, requestOptions(options, admittedControls)));
23
+ const admit = (input, options) => admitSource(input, mergeOptions(options, admittedControls));
24
+ const call = ((input, options) => callSource(input, requestOptions(options, admittedControls)));
25
+ const invoke = ((reference, input, options) => invokeSource(reference, input, requestOptions(options, admittedControls)));
19
26
  const stream = (input, options) => streamSource(input, requestOptions(options, admittedControls));
20
27
  const binary = (input, options) => binarySource(input, requestOptions(options, admittedControls));
28
+ const upload = (input, options) => uploadSource(input, mergeOptions(options, admittedControls));
29
+ const download = (location, options) => downloadSource(location, controlOptions(options, admittedControls));
30
+ const content = Object.freeze({ upload, download });
21
31
  const viewsFor = (target, options) => viewsForSource(target, controlOptions(options, admittedControls));
22
32
  const install = (request, options) => schemaSource.install(request, controlOptions(options, admittedControls));
23
33
  const uninstall = (request, options) => schemaSource.uninstall(request, controlOptions(options, admittedControls));
@@ -26,7 +36,7 @@ export function bindSession(source, controls) {
26
36
  const dependencies = (origin, options) => schemaSource.dependencies(origin, controlOptions(options, admittedControls));
27
37
  const schema = Object.freeze({ install, uninstall, inspect, bundle, dependencies });
28
38
  const graph = createGraph(call);
29
- const auth = createAuth((target, input, options) => call(createCall(target, input), options));
39
+ const auth = createAuth((target, input, options) => call(createCall(target, input), options), admit);
30
40
  return Object.freeze({
31
41
  ...graph,
32
42
  kernel: source.kernel,
@@ -37,9 +47,18 @@ export function bindSession(source, controls) {
37
47
  invoke,
38
48
  stream,
39
49
  binary,
50
+ content,
40
51
  viewsFor,
41
52
  });
42
53
  }
54
+ const unavailableContent = Object.freeze({
55
+ upload: (() => {
56
+ throw new TypeError('Content transfer is unavailable from this bound Session source.');
57
+ }),
58
+ download: () => {
59
+ throw new TypeError('Content transfer is unavailable from this bound Session source.');
60
+ },
61
+ });
43
62
  function admitControls(input) {
44
63
  if (input === undefined)
45
64
  return undefined;
@@ -0,0 +1,30 @@
1
+ import type { schema } from '@astrale-os/kernel-dsl/v1';
2
+ import type { Ref } from '@astrale-os/kernel-protocol/artifact';
3
+ import { Path } from '@astrale-os/kernel-core/path';
4
+ import type { Binary, Input, Result } from '../client/index.js';
5
+ import type { SessionControlOptions, SessionOperation } from './context/index.js';
6
+ import type { CallableReference } from './reference.js';
7
+ import type { SessionRequestOptionsWithBody } from './routing/index.js';
8
+ export interface ContentLocation {
9
+ readonly node: string;
10
+ readonly property: string;
11
+ }
12
+ export interface ContentUploadOptions extends SessionControlOptions {
13
+ readonly mediaType: string;
14
+ }
15
+ export interface ContentUploadResult {
16
+ readonly receipt: schema.values.BlobReceipt;
17
+ readonly ref: Ref;
18
+ readonly expiresAt: string;
19
+ }
20
+ export interface ContentApi {
21
+ upload(source: Blob | Uint8Array, options: ContentUploadOptions): Promise<ContentUploadResult>;
22
+ download(location: ContentLocation, options?: SessionControlOptions): Promise<Binary>;
23
+ }
24
+ export interface ContentSessionDependencies {
25
+ readonly operation: (options: SessionControlOptions) => SessionOperation;
26
+ readonly invoke: (reference: CallableReference<unknown, unknown, 'binary'>, input: unknown, options: SessionRequestOptionsWithBody, operation: SessionOperation) => Promise<unknown>;
27
+ readonly dispatch: (target: Path, input: Input, operation: SessionOperation, body?: Blob) => Promise<Result>;
28
+ }
29
+ /** Transfer immutable content over the authenticated Session routing conversation. */
30
+ export declare function createContentApi(dependencies: ContentSessionDependencies): ContentApi;