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

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 (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/deployment/adapter/adapter.d.ts +3 -3
  3. package/dist/deployment/adapter/define.js +3 -3
  4. package/dist/deployment/adapter/development/context.d.ts +14 -0
  5. package/dist/deployment/adapter/development/index.d.ts +4 -0
  6. package/dist/deployment/adapter/{watch/context.d.ts → development/input.d.ts} +3 -5
  7. package/dist/deployment/adapter/development/placement.d.ts +5 -0
  8. package/dist/deployment/adapter/development/placement.js +1 -0
  9. package/dist/deployment/adapter/development/session.d.ts +9 -0
  10. package/dist/deployment/adapter/development/session.js +1 -0
  11. package/dist/deployment/adapter/index.d.ts +1 -1
  12. package/dist/deployment/adapter/prepare/context.d.ts +7 -0
  13. package/dist/deployment/index.d.ts +1 -1
  14. package/dist/execution/identity/authentication.d.ts +2 -1
  15. package/dist/execution/identity/authentication.js +3 -3
  16. package/dist/execution/identity/exchange.d.ts +2 -3
  17. package/dist/execution/identity/exchange.js +6 -2
  18. package/dist/execution/identity/identity.js +4 -1
  19. package/dist/execution/identity/jwks.d.ts +9 -1
  20. package/dist/execution/identity/jwks.js +126 -2
  21. package/dist/tooling/cli/arguments.d.ts +0 -1
  22. package/dist/tooling/cli/arguments.js +41 -13
  23. package/dist/tooling/cli/bun.d.ts +1 -0
  24. package/dist/tooling/cli/bun.js +7 -0
  25. package/dist/tooling/cli/development/develop.d.ts +8 -0
  26. package/dist/tooling/cli/development/develop.js +284 -0
  27. package/dist/tooling/cli/development/lifecycle.d.ts +9 -0
  28. package/dist/tooling/cli/development/lifecycle.js +39 -0
  29. package/dist/tooling/cli/development/project-lock.d.ts +17 -0
  30. package/dist/tooling/cli/development/project-lock.js +193 -0
  31. package/dist/tooling/cli/development/rebuild.d.ts +13 -0
  32. package/dist/tooling/cli/development/rebuild.js +58 -0
  33. package/dist/tooling/cli/development/source-watch.d.ts +14 -0
  34. package/dist/tooling/cli/development/source-watch.js +92 -0
  35. package/dist/tooling/cli/orchestrate.js +23 -64
  36. package/dist/tooling/linter/implementations/source/global.js +4 -2
  37. package/dist/tooling/linter/implementations/source/views.js +23 -0
  38. package/dist/tooling/linter/policy/generated.js +2 -2
  39. package/dist/tooling/linter/requirements/registry.js +1 -0
  40. package/package.json +1 -1
  41. package/dist/deployment/adapter/watch/handle.d.ts +0 -5
  42. package/dist/deployment/adapter/watch/index.d.ts +0 -2
  43. /package/dist/deployment/adapter/{watch → development}/context.js +0 -0
  44. /package/dist/deployment/adapter/{watch → development}/index.js +0 -0
  45. /package/dist/deployment/adapter/{watch/handle.js → development/input.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0-beta.69](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.68...sdk-v0.5.0-beta.69) (2026-08-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * add coordinated Domain development sessions ([#300](https://github.com/astrale-os/sdk/issues/300)) ([102104f](https://github.com/astrale-os/sdk/commit/102104f1ed55333f5892db23c036578aa0ffd2e1))
9
+ * **tooling:** keep frontend composition application-owned ([#310](https://github.com/astrale-os/sdk/issues/310)) ([31e9bb6](https://github.com/astrale-os/sdk/commit/31e9bb6de256ef9cdbcdb0296abb973086f11ff0))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **adapter-astrale:** preserve explicit Services installation ownership ([#305](https://github.com/astrale-os/sdk/issues/305)) ([1b9dd64](https://github.com/astrale-os/sdk/commit/1b9dd6422e5d80a552ec5a70b5d262b8413dd6bc))
15
+ * **cli:** skip unused secrets files ([3a1c534](https://github.com/astrale-os/sdk/commit/3a1c53439d5172add9c2c724060daaffc0a517bb))
16
+
17
+ ## [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)
18
+
19
+
20
+ ### Performance Improvements
21
+
22
+ * **execution:** cache issuer verification basis ([#307](https://github.com/astrale-os/sdk/issues/307)) ([a88f227](https://github.com/astrale-os/sdk/commit/a88f22700e61f5c5926621f95fc66638b462fbf5))
23
+
3
24
  ## [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
25
 
5
26
 
@@ -1,13 +1,13 @@
1
1
  import type { DeployContext, DeployResult } from './deploy/index.js';
2
+ import type { DevelopmentContext, DevelopmentSession } from './development/index.js';
2
3
  import type { ArtifactSource, PrepareContext } from './prepare/index.js';
3
- import type { WatchContext, WatchHandle } from './watch/index.js';
4
4
  export interface Adapter<Parameters = unknown> {
5
5
  readonly kind: 'adapter';
6
6
  readonly name: string;
7
7
  readonly version: string;
8
8
  parameters(environment: string): Parameters;
9
9
  prepare(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
10
- watch(parameters: Parameters, context: WatchContext): Promise<WatchHandle>;
10
+ develop(parameters: Parameters, context: DevelopmentContext): Promise<DevelopmentSession>;
11
11
  deploy(parameters: Parameters, context: DeployContext): Promise<DeployResult>;
12
12
  readonly secretsFile?: (parameters: Parameters) => string | undefined;
13
13
  }
@@ -16,7 +16,7 @@ export interface AdapterInput<Parameters> {
16
16
  readonly version: string;
17
17
  readonly environments: Readonly<Record<string, Parameters>>;
18
18
  prepare(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
19
- watch(parameters: Parameters, context: WatchContext): Promise<WatchHandle>;
19
+ develop(parameters: Parameters, context: DevelopmentContext): Promise<DevelopmentSession>;
20
20
  deploy(parameters: Parameters, context: DeployContext): Promise<DeployResult>;
21
21
  readonly secretsFile?: (parameters: Parameters) => string | undefined;
22
22
  }
@@ -2,7 +2,7 @@ const admittedAdapters = new WeakSet();
2
2
  /** Capture one receiver-bound provider adapter and its environment parameters. */
3
3
  export function defineAdapter(input) {
4
4
  const value = record(input, 'Adapter definition');
5
- exact(value, ['name', 'version', 'environments', 'prepare', 'watch', 'deploy'], ['secretsFile']);
5
+ exact(value, ['name', 'version', 'environments', 'prepare', 'develop', 'deploy'], ['secretsFile']);
6
6
  const name = stableName(input.name);
7
7
  const version = stableName(input.version);
8
8
  const environments = record(input.environments, 'Adapter environments');
@@ -13,7 +13,7 @@ export function defineAdapter(input) {
13
13
  const names = Object.keys(capturedEnvironments).sort();
14
14
  const receiver = Object.freeze({ ...input, environments: capturedEnvironments });
15
15
  const prepare = capture(receiver, 'prepare');
16
- const watch = capture(receiver, 'watch');
16
+ const develop = capture(receiver, 'develop');
17
17
  const deploy = capture(receiver, 'deploy');
18
18
  const secretsFile = optional(receiver, 'secretsFile');
19
19
  const adapter = Object.freeze({
@@ -27,7 +27,7 @@ export function defineAdapter(input) {
27
27
  throw new TypeError(`Adapter ${name} has no environment ${environment}; known environments: ${names.length === 0 ? '(none)' : names.join(', ')}.`);
28
28
  },
29
29
  prepare,
30
- watch,
30
+ develop,
31
31
  deploy,
32
32
  ...(secretsFile === undefined ? {} : { secretsFile }),
33
33
  });
@@ -0,0 +1,14 @@
1
+ import type { DevelopmentInput } from './input.js';
2
+ import type { DevelopmentPlacement } from './placement.js';
3
+ export interface DevelopmentOverrides {
4
+ readonly publicUrl?: string;
5
+ readonly localPort?: number;
6
+ }
7
+ /** Provider-neutral inputs retained for one complete development session. */
8
+ export interface DevelopmentContext extends DevelopmentInput {
9
+ readonly projectDir: string;
10
+ readonly environment: string;
11
+ readonly overrides: DevelopmentOverrides;
12
+ readonly signal: AbortSignal;
13
+ onPlacement(placement: DevelopmentPlacement): Promise<void>;
14
+ }
@@ -0,0 +1,4 @@
1
+ export type { DevelopmentContext, DevelopmentOverrides } from './context.js';
2
+ export type { DevelopmentInput } from './input.js';
3
+ export type { DevelopmentPlacement } from './placement.js';
4
+ export type { DevelopmentSession } from './session.js';
@@ -1,8 +1,6 @@
1
1
  import type { PreparedArtifact } from '../prepare/index.js';
2
- export interface WatchContext {
3
- readonly projectDir: string;
4
- readonly environment: string;
5
- readonly secrets: Readonly<Record<string, string>>;
2
+ /** One admitted application artifact and its declared provider secrets. */
3
+ export interface DevelopmentInput {
6
4
  readonly artifact: PreparedArtifact;
7
- onReload(): void;
5
+ readonly secrets: Readonly<Record<string, string>>;
8
6
  }
@@ -0,0 +1,5 @@
1
+ /** Exact serving coordinates committed by one development placement generation. */
2
+ export interface DevelopmentPlacement {
3
+ readonly releaseUrl: string;
4
+ readonly localUrl?: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { DevelopmentInput } from './input.js';
2
+ import type { DevelopmentPlacement } from './placement.js';
3
+ /** Lifecycle capability for one ready development placement. */
4
+ export interface DevelopmentSession {
5
+ current(): DevelopmentPlacement;
6
+ update(input: DevelopmentInput): Promise<void>;
7
+ readonly finished: Promise<void>;
8
+ stop(): Promise<void>;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  export type { Adapter, AdapterInput } from './adapter.js';
2
2
  export { defineAdapter, isAdapter } from './define.js';
3
3
  export type * from './prepare/index.js';
4
- export type * from './watch/index.js';
4
+ export type * from './development/index.js';
5
5
  export type * from './deploy/index.js';
@@ -3,6 +3,13 @@ import type { ResolvedRuntimeReference } from '../../runtime/index.js';
3
3
  export interface PrepareContext<BuildValue extends Build = Build> {
4
4
  readonly projectDir: string;
5
5
  readonly environment: string;
6
+ /**
7
+ * Preparation purpose. Development may omit immutable frontend output because
8
+ * the live provider serves the declared frontend source directly; release
9
+ * preparation must remain complete. Omitted by older custom callers means
10
+ * release for compatibility.
11
+ */
12
+ readonly mode?: 'development' | 'release';
6
13
  readonly build: BuildValue;
7
14
  readonly runtime: ResolvedRuntimeReference;
8
15
  }
@@ -1,5 +1,5 @@
1
1
  export { defineAdapter, isAdapter } from './adapter/index.js';
2
- export type { Adapter, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, DeployContext, DeploymentPhase, DeploymentProgress, DeployResult, FailedDeployResult, MutationKnowledge, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, WatchContext, WatchHandle, } from './adapter/index.js';
2
+ export type { Adapter, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, DeployContext, DevelopmentContext, DevelopmentInput, DevelopmentOverrides, DevelopmentPlacement, DevelopmentSession, DeploymentPhase, DeploymentProgress, DeployResult, FailedDeployResult, MutationKnowledge, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, } from './adapter/index.js';
3
3
  export { compile, isBuild } from './build/index.js';
4
4
  export type { Build, BuildBundle, BuildSchema } from './build/index.js';
5
5
  export { deploy, isDeployment } from './deploy.js';
@@ -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 {
@@ -2,7 +2,6 @@ import type { LintReportFormat } from '../linter/index.js';
2
2
  export interface ParsedArgs {
3
3
  readonly command: 'dev' | 'build' | 'deploy' | 'lint' | 'package';
4
4
  readonly env: string;
5
- readonly watch: boolean;
6
5
  readonly fix?: boolean;
7
6
  readonly format?: LintReportFormat;
8
7
  readonly port?: number;
@@ -1,7 +1,7 @@
1
+ import { isIP } from 'node:net';
1
2
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
2
3
  export function parseArgs(argv) {
3
4
  const [command, ...rest] = argv;
4
- const watch = rest.includes('--watch');
5
5
  const fix = rest.includes('--fix');
6
6
  let port;
7
7
  let host;
@@ -13,13 +13,13 @@ export function parseArgs(argv) {
13
13
  if (argument === '--port') {
14
14
  const value = rest[++index];
15
15
  port = Number(value);
16
- if (!Number.isInteger(port) || port <= 0) {
16
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
17
17
  throw new Error(`--port needs a port number, got "${value}"`);
18
18
  }
19
19
  }
20
20
  else if (argument.startsWith('--port=')) {
21
21
  port = Number(argument.slice('--port='.length));
22
- if (!Number.isInteger(port) || port <= 0) {
22
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
23
23
  throw new Error(`--port needs a port number, got "${argument}"`);
24
24
  }
25
25
  }
@@ -41,7 +41,7 @@ export function parseArgs(argv) {
41
41
  else if (argument.startsWith('--environment=')) {
42
42
  environment = requiredValue('--environment', argument.slice('--environment='.length));
43
43
  }
44
- else if (argument === '--watch' || argument === '--fix') {
44
+ else if (argument === '--fix') {
45
45
  continue;
46
46
  }
47
47
  else if (argument.startsWith('-')) {
@@ -56,9 +56,6 @@ export function parseArgs(argv) {
56
56
  if (format !== undefined && command !== 'lint') {
57
57
  throw new Error('`--format` is only valid for `lint`.');
58
58
  }
59
- if (watch && !['dev', 'deploy'].includes(command ?? '')) {
60
- throw new Error('`--watch` is only valid for the dev / deploy commands.');
61
- }
62
59
  if ((port !== undefined || host !== undefined) && command !== 'dev') {
63
60
  throw new Error('`--port` and `--host` are only valid for `dev`.');
64
61
  }
@@ -76,7 +73,6 @@ export function parseArgs(argv) {
76
73
  return {
77
74
  command: 'dev',
78
75
  env: environment ?? positionals[0] ?? 'dev',
79
- watch: true,
80
76
  ...(port !== undefined ? { port } : {}),
81
77
  ...(host !== undefined ? { host } : {}),
82
78
  };
@@ -89,24 +85,22 @@ export function parseArgs(argv) {
89
85
  return {
90
86
  command: 'deploy',
91
87
  env,
92
- watch,
93
88
  };
94
89
  }
95
90
  case 'build':
96
91
  if (positionals.length > 0)
97
92
  throw new Error('`build` does not accept positional arguments.');
98
- return { command: 'build', env: 'dev', watch: false };
93
+ return { command: 'build', env: 'dev' };
99
94
  case 'package':
100
95
  if (positionals.length > 0)
101
96
  throw new Error('`package` does not accept positional arguments.');
102
- return { command: 'package', env: 'dev', watch: false };
97
+ return { command: 'package', env: 'dev' };
103
98
  case 'lint':
104
99
  if (positionals.length > 0)
105
100
  throw new Error('`lint` does not accept positional arguments.');
106
101
  return {
107
102
  command: 'lint',
108
103
  env: 'dev',
109
- watch: false,
110
104
  ...(fix ? { fix: true } : {}),
111
105
  ...(format ? { format } : {}),
112
106
  };
@@ -119,12 +113,46 @@ function normalizeHost(input) {
119
113
  throw new Error(`--host needs a public URL, got "${input ?? ''}"`);
120
114
  }
121
115
  try {
122
- return new URL(/^https?:\/\//u.test(input) ? input : `https://${input}`).origin;
116
+ const url = new URL(input);
117
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
118
+ url.username !== '' ||
119
+ url.password !== '' ||
120
+ url.pathname !== '/' ||
121
+ url.search !== '' ||
122
+ url.hash !== '' ||
123
+ privateHostname(url.hostname))
124
+ throw new Error('invalid');
125
+ return url.origin;
123
126
  }
124
127
  catch {
125
128
  throw new Error(`--host needs a valid URL, got "${input}"`);
126
129
  }
127
130
  }
131
+ function privateHostname(input) {
132
+ const host = input
133
+ .toLowerCase()
134
+ .replace(/^\[|\]$/gu, '')
135
+ .replace(/\.$/u, '');
136
+ if (host === 'localhost' || host.endsWith('.localhost'))
137
+ return true;
138
+ if (isIP(host) === 4) {
139
+ const parts = host.split('.').map(Number);
140
+ return (parts[0] === 0 ||
141
+ parts[0] === 10 ||
142
+ parts[0] === 127 ||
143
+ (parts[0] === 169 && parts[1] === 254) ||
144
+ (parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) ||
145
+ (parts[0] === 192 && parts[1] === 168));
146
+ }
147
+ if (isIP(host) !== 6)
148
+ return false;
149
+ return (host === '::' ||
150
+ host === '::1' ||
151
+ host.startsWith('fc') ||
152
+ host.startsWith('fd') ||
153
+ /^fe[89ab]/u.test(host) ||
154
+ /^::ffff:(?:0|10|127|169\.254|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./u.test(host));
155
+ }
128
156
  function lintFormat(input) {
129
157
  if (input !== 'stylish' && input !== 'json') {
130
158
  throw new Error(`--format must be "stylish" or "json", got "${input ?? ''}"`);
@@ -10,5 +10,6 @@ export type BunOutcome = {
10
10
  };
11
11
  /** Re-enter the published executable under Bun only when authored TypeScript must be imported. */
12
12
  export declare function runWithBun(executable: string, argv: readonly string[]): Promise<BunOutcome>;
13
+ export declare function isolateDevelopmentProcessGroup(argv: readonly string[], platform?: NodeJS.Platform): boolean;
13
14
  /** Prefer the generated project's exact Bun package binary over an ambient executable or shim. */
14
15
  export declare function resolveBunExecutable(cwd: string): string;
@@ -14,6 +14,10 @@ export function runWithBun(executable, argv) {
14
14
  return new Promise((resolve, reject) => {
15
15
  const child = spawn(resolveBunExecutable(process.cwd()), [executable, ...argv], {
16
16
  stdio: 'inherit',
17
+ // Keep a long-lived development runtime outside the launcher's terminal
18
+ // process group. The launcher receives Ctrl-C, forwards it exactly once,
19
+ // and remains alive until Bun finishes provider and Kernel cleanup.
20
+ detached: isolateDevelopmentProcessGroup(argv),
17
21
  });
18
22
  const handlers = FORWARDED_SIGNALS.map((signal) => [signal, () => child.kill(signal)]);
19
23
  for (const [signal, handler] of handlers)
@@ -40,6 +44,9 @@ export function runWithBun(executable, argv) {
40
44
  });
41
45
  });
42
46
  }
47
+ export function isolateDevelopmentProcessGroup(argv, platform = process.platform) {
48
+ return platform !== 'win32' && argv[0] === 'dev';
49
+ }
43
50
  /** Prefer the generated project's exact Bun package binary over an ambient executable or shim. */
44
51
  export function resolveBunExecutable(cwd) {
45
52
  const projectRequire = createRequire(join(cwd, 'package.json'));
@@ -0,0 +1,8 @@
1
+ import type { DevelopmentOverrides } from '../../../deployment/adapter/index.js';
2
+ /** Run one complete provider-neutral development session until interruption. */
3
+ export declare function develop(input: {
4
+ readonly configPath: string;
5
+ readonly projectDir: string;
6
+ readonly environment: string;
7
+ readonly overrides: DevelopmentOverrides;
8
+ }): Promise<number>;