@astrale-os/sdk 0.5.0-beta.67 → 0.5.0-beta.68

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0-beta.68](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.67...sdk-v0.5.0-beta.68) (2026-08-28)
4
+
5
+
6
+ ### Performance Improvements
7
+
8
+ * **execution:** cache issuer verification basis ([#307](https://github.com/astrale-os/sdk/issues/307)) ([a88f227](https://github.com/astrale-os/sdk/commit/a88f22700e61f5c5926621f95fc66638b462fbf5))
9
+
3
10
  ## [0.5.0-beta.67](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.66...sdk-v0.5.0-beta.67) (2026-08-28)
4
11
 
5
12
 
@@ -2,7 +2,7 @@ import type { Fetch } from '@astrale-os/kernel-client';
2
2
  import type { CredentialInput, Grant, IssuerId } from '../../platform/auth/index.js';
3
3
  import type { AuthenticatedInvocation } from '../invocation/index.js';
4
4
  import type { InvocationAdmissionInput } from '../invocation/index.js';
5
- import type { IdentityNetworkPolicy, VerificationBasis } from './jwks.js';
5
+ import type { IdentityNetworkPolicy, VerificationBasis, VerificationBasisResolver } from './jwks.js';
6
6
  import type { SigningMaterial } from './signing.js';
7
7
  export declare function admit(request: InvocationAdmissionInput, configuration: AuthenticationConfiguration): Promise<unknown>;
8
8
  export interface AuthenticationConfiguration {
@@ -11,6 +11,7 @@ export interface AuthenticationConfiguration {
11
11
  readonly signing: SigningMaterial;
12
12
  readonly fetch: Fetch;
13
13
  readonly policy: IdentityNetworkPolicy;
14
+ readonly verification: VerificationBasisResolver;
14
15
  }
15
16
  export declare function authenticate(credentialInput: CredentialInput, expectedKernel: IssuerId, execution: {
16
17
  readonly deadline: number;
@@ -48,7 +48,7 @@ export async function admit(request, configuration) {
48
48
  }
49
49
  }
50
50
  export async function authenticate(credentialInput, expectedKernel, execution, configuration) {
51
- const established = await establishCaller(credentialInput, configuration.issuer, configuration.fetch, execution, configuration.policy);
51
+ const established = await establishCaller(credentialInput, configuration.issuer, configuration.verification, execution, configuration.policy);
52
52
  if (established.verified.iss !== expectedKernel) {
53
53
  throw protocolError('AUTH_INVALID', 'Credential is invalid.');
54
54
  }
@@ -104,10 +104,10 @@ export async function authenticate(credentialInput, expectedKernel, execution, c
104
104
  });
105
105
  }
106
106
  }
107
- async function establishCaller(input, audience, fetch, execution, policy) {
107
+ async function establishCaller(input, audience, verification, execution, policy) {
108
108
  requireLive(execution);
109
109
  try {
110
- const context = await verifyCredential(input, audience, fetch, execution.signal, policy);
110
+ const context = await verifyCredential(input, audience, verification, execution.signal, policy);
111
111
  const carried = delegation.accept(context.verified.claims.delegation);
112
112
  if (typeof carried.credential !== 'string') {
113
113
  throw new CredentialRejected('Destination carrier has no Kernel authority proof.');
@@ -1,10 +1,9 @@
1
- import type { Fetch } from '@astrale-os/kernel-client';
2
1
  import type { exchange } from '@astrale-os/kernel-server';
3
2
  import type { IssuerId } from '../../platform/auth/index.js';
4
- import type { IdentityNetworkPolicy } from './jwks.js';
3
+ import type { IdentityNetworkPolicy, VerificationBasisResolver } from './jwks.js';
5
4
  import type { SigningMaterial } from './signing.js';
6
5
  export interface ExchangeOptions {
7
6
  readonly maximumTtlSeconds?: number;
8
7
  }
9
8
  export declare function maximumExchangeTtl(input: ExchangeOptions | undefined): number;
10
- export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial, fetch: Fetch, policy: IdentityNetworkPolicy, maximumTtlSeconds: number): exchange.Provider;
9
+ export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial, policy: IdentityNetworkPolicy, verification: VerificationBasisResolver, maximumTtlSeconds: number): exchange.Provider;
@@ -22,19 +22,22 @@ export function maximumExchangeTtl(input) {
22
22
  }
23
23
  return value;
24
24
  }
25
- export function tokenExchange(issuer, subject, signing, fetch, policy, maximumTtlSeconds) {
25
+ export function tokenExchange(issuer, subject, signing, policy, verification, maximumTtlSeconds) {
26
26
  return Object.freeze({
27
27
  async exchange(request, context) {
28
28
  try {
29
- const outer = await verifyCredential(request.token, issuer, fetch, context.signal, policy);
29
+ context.signal.throwIfAborted();
30
+ const outer = await verifyCredential(request.token, issuer, verification, context.signal, policy);
30
31
  requireProfile(outer.verified, 'outer');
31
32
  const carried = delegation.accept(outer.verified.claims.delegation);
32
33
  if (typeof carried.credential !== 'string')
33
34
  invalid();
35
+ context.signal.throwIfAborted();
34
36
  const inner = await verifyCredentialAgainst(carried.credential, outer.verified.iss, outer.basis);
35
37
  requireProfile(inner.verified, 'inner');
36
38
  requireCoherence(outer.verified, inner.verified);
37
39
  acceptResolvedDelegation(inner.verified.claims.delegation);
40
+ context.signal.throwIfAborted();
38
41
  const now = Math.floor(Date.now() / 1_000);
39
42
  const expiresAt = Math.min(outer.verified.exp, inner.verified.exp, now + maximumTtlSeconds);
40
43
  if (expiresAt <= now)
@@ -55,6 +58,7 @@ export function tokenExchange(issuer, subject, signing, fetch, policy, maximumTt
55
58
  .setIssuedAt(now)
56
59
  .setExpirationTime(expiresAt)
57
60
  .sign(await signing.privateKey);
61
+ context.signal.throwIfAborted();
58
62
  return issuerExchange.response(token, expiresAt);
59
63
  }
60
64
  catch (cause) {
@@ -2,6 +2,7 @@ import { admit, authenticate } from './authentication.js';
2
2
  import { publicationDelivery } from './delivery.js';
3
3
  import { httpIssuer } from './errors.js';
4
4
  import { maximumExchangeTtl, tokenExchange } from './exchange.js';
5
+ import { createVerificationBasisResolver } from './jwks.js';
5
6
  import { signing } from './signing.js';
6
7
  /** Create one stateless execution identity backed by an asymmetric private JWK. */
7
8
  export function createIdentity(input) {
@@ -25,16 +26,18 @@ export function createIdentity(input) {
25
26
  ...(input.allowEndpoint === undefined ? {} : { allowEndpoint: input.allowEndpoint }),
26
27
  ...(input.allowInsecureHttp === true ? { allowInsecureHttp: true } : {}),
27
28
  });
29
+ const verification = createVerificationBasisResolver(fetch, policy);
28
30
  const configuration = Object.freeze({
29
31
  issuer,
30
32
  subject: input.subject,
31
33
  signing: material,
32
34
  fetch,
33
35
  policy,
36
+ verification,
34
37
  });
35
38
  const exchangeProvider = input.tokenExchange === false
36
39
  ? undefined
37
- : tokenExchange(issuer, input.subject, material, fetch, policy, maximumExchangeTtl(input.tokenExchange));
40
+ : tokenExchange(issuer, input.subject, material, policy, verification, maximumExchangeTtl(input.tokenExchange));
38
41
  return identity({
39
42
  issuer,
40
43
  subject: input.subject,
@@ -10,7 +10,15 @@ export interface VerificationBasis {
10
10
  readonly algorithms: readonly [string, ...string[]];
11
11
  readonly keys: readonly JsonWebKey[];
12
12
  }
13
- export declare function verifyCredential(input: CredentialInput, audience: IssuerId, fetch: Fetch, signal: AbortSignal, policy: IdentityNetworkPolicy): Promise<{
13
+ export interface VerificationBasisResolver {
14
+ resolve(issuer: IssuerId, keyId: string | undefined, signal: AbortSignal): Promise<VerificationBasis>;
15
+ }
16
+ /**
17
+ * Retain only recently admitted public issuer material for one execution Identity.
18
+ * An unseen key ID may refresh once per cooldown; failures never replace a usable basis.
19
+ */
20
+ export declare function createVerificationBasisResolver(fetch: Fetch, policy: IdentityNetworkPolicy, now?: () => number): VerificationBasisResolver;
21
+ export declare function verifyCredential(input: CredentialInput, audience: IssuerId, bases: VerificationBasisResolver, signal: AbortSignal, policy: IdentityNetworkPolicy): Promise<{
14
22
  readonly raw: CredentialInput;
15
23
  readonly verified: VerifiedCredential;
16
24
  readonly basis: VerificationBasis;
@@ -5,7 +5,94 @@ import { errors as joseErrors, importJWK, jwtVerify } from 'jose';
5
5
  import { credential } from '../../platform/auth/index.js';
6
6
  import { CredentialRejected, IssuerUnavailable, admitEndpoint, httpIssuer } from './errors.js';
7
7
  const MAXIMUM_DISCOVERY_BYTES = 256 * 1024;
8
- export async function verifyCredential(input, audience, fetch, signal, policy) {
8
+ const MAXIMUM_VERIFICATION_BASES = 64;
9
+ const VERIFICATION_BASIS_TTL_MS = 60_000;
10
+ const UNKNOWN_KEY_REFRESH_COOLDOWN_MS = 60_000;
11
+ /**
12
+ * Retain only recently admitted public issuer material for one execution Identity.
13
+ * An unseen key ID may refresh once per cooldown; failures never replace a usable basis.
14
+ */
15
+ export function createVerificationBasisResolver(fetch, policy, now = Date.now) {
16
+ const bases = new Map();
17
+ const pending = new Map();
18
+ return Object.freeze({
19
+ async resolve(issuer, keyId, signal) {
20
+ signal.throwIfAborted();
21
+ const cached = bases.get(issuer);
22
+ const observedAt = now();
23
+ const age = cached === undefined ? Number.POSITIVE_INFINITY : observedAt - cached.refreshedAt;
24
+ const fresh = cached !== undefined && age >= 0 && age < VERIFICATION_BASIS_TTL_MS;
25
+ const knownKey = keyId === undefined ||
26
+ cached?.basis.keys.some((candidate) => candidate.kid === keyId) === true;
27
+ const unknownKeyCooldown = cached?.unknownKeyRefreshedAt !== undefined &&
28
+ observedAt - cached.unknownKeyRefreshedAt >= 0 &&
29
+ observedAt - cached.unknownKeyRefreshedAt < UNKNOWN_KEY_REFRESH_COOLDOWN_MS;
30
+ if (fresh && knownKey) {
31
+ bases.delete(issuer);
32
+ bases.set(issuer, cached);
33
+ return cached.basis;
34
+ }
35
+ const unknownKeyRefresh = cached !== undefined && !knownKey;
36
+ let refresh = pending.get(issuer);
37
+ if (refresh === undefined && fresh && unknownKeyCooldown) {
38
+ bases.delete(issuer);
39
+ bases.set(issuer, cached);
40
+ return cached.basis;
41
+ }
42
+ if (refresh === undefined && unknownKeyRefresh && cached !== undefined) {
43
+ bases.set(issuer, Object.freeze({ ...cached, unknownKeyRefreshedAt: observedAt }));
44
+ }
45
+ if (refresh === undefined) {
46
+ const controller = new AbortController();
47
+ refresh = {
48
+ controller,
49
+ operation: undefined,
50
+ subscribers: 0,
51
+ settled: false,
52
+ unknownKeyRefresh,
53
+ requestedKeyIds: new Set(keyId === undefined ? [] : [keyId]),
54
+ };
55
+ const owned = refresh;
56
+ owned.operation = discover(issuer, fetch, controller.signal, policy)
57
+ .then((basis) => {
58
+ controller.signal.throwIfAborted();
59
+ const refreshedAt = now();
60
+ if (bases.size >= MAXIMUM_VERIFICATION_BASES && !bases.has(issuer)) {
61
+ const oldest = bases.keys().next().value;
62
+ if (oldest !== undefined)
63
+ bases.delete(oldest);
64
+ }
65
+ bases.delete(issuer);
66
+ bases.set(issuer, Object.freeze({
67
+ basis,
68
+ refreshedAt,
69
+ ...(owned.unknownKeyRefresh ||
70
+ [...owned.requestedKeyIds].some((requested) => !basis.keys.some((candidate) => candidate.kid === requested))
71
+ ? { unknownKeyRefreshedAt: refreshedAt }
72
+ : {}),
73
+ }));
74
+ return basis;
75
+ })
76
+ .finally(() => {
77
+ owned.settled = true;
78
+ if (pending.get(issuer) === owned)
79
+ pending.delete(issuer);
80
+ });
81
+ pending.set(issuer, owned);
82
+ }
83
+ else if (unknownKeyRefresh) {
84
+ refresh.unknownKeyRefresh = true;
85
+ }
86
+ if (keyId !== undefined)
87
+ refresh.requestedKeyIds.add(keyId);
88
+ const basis = await subscribe(refresh, issuer, pending, signal);
89
+ signal.throwIfAborted();
90
+ return basis;
91
+ },
92
+ });
93
+ }
94
+ export async function verifyCredential(input, audience, bases, signal, policy) {
95
+ signal.throwIfAborted();
9
96
  let inspected;
10
97
  try {
11
98
  inspected = credential.inspect(input);
@@ -22,11 +109,48 @@ export async function verifyCredential(input, audience, fetch, signal, policy) {
22
109
  catch (cause) {
23
110
  throw new CredentialRejected('Credential issuer is invalid.', cause);
24
111
  }
25
- const basis = await discover(admittedIssuer, fetch, signal, policy);
112
+ const basis = await bases.resolve(admittedIssuer, inspected.kid, signal);
113
+ signal.throwIfAborted();
26
114
  const verified = await verifyCredentialAgainst(input, audience, basis, inspected);
115
+ signal.throwIfAborted();
27
116
  admitEndpoint(`${admittedIssuer.replace(/\/+$/u, '')}/invoke`, policy);
28
117
  return Object.freeze({ raw: input, verified: verified.verified, basis });
29
118
  }
119
+ function subscribe(refresh, issuer, pending, signal) {
120
+ signal.throwIfAborted();
121
+ refresh.subscribers += 1;
122
+ return new Promise((resolve, reject) => {
123
+ let complete = false;
124
+ const finish = () => {
125
+ if (complete)
126
+ return false;
127
+ complete = true;
128
+ signal.removeEventListener('abort', cancel);
129
+ refresh.subscribers -= 1;
130
+ if (!refresh.settled && refresh.subscribers === 0) {
131
+ if (pending.get(issuer) === refresh)
132
+ pending.delete(issuer);
133
+ refresh.controller.abort(new DOMException('Issuer discovery was cancelled.', 'AbortError'));
134
+ }
135
+ return true;
136
+ };
137
+ const cancel = () => {
138
+ if (!finish())
139
+ return;
140
+ reject(signal.reason);
141
+ };
142
+ signal.addEventListener('abort', cancel, { once: true });
143
+ refresh.operation.then((basis) => {
144
+ if (!finish())
145
+ return;
146
+ resolve(basis);
147
+ }, (cause) => {
148
+ if (!finish())
149
+ return;
150
+ reject(cause);
151
+ });
152
+ });
153
+ }
30
154
  export async function verifyCredentialAgainst(input, audience, basis, preinspected) {
31
155
  let inspected;
32
156
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.67",
3
+ "version": "0.5.0-beta.68",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",