@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.
- package/CHANGELOG.md +21 -0
- package/dist/deployment/adapter/adapter.d.ts +3 -3
- package/dist/deployment/adapter/define.js +3 -3
- package/dist/deployment/adapter/development/context.d.ts +14 -0
- package/dist/deployment/adapter/development/index.d.ts +4 -0
- package/dist/deployment/adapter/{watch/context.d.ts → development/input.d.ts} +3 -5
- package/dist/deployment/adapter/development/placement.d.ts +5 -0
- package/dist/deployment/adapter/development/placement.js +1 -0
- package/dist/deployment/adapter/development/session.d.ts +9 -0
- package/dist/deployment/adapter/development/session.js +1 -0
- package/dist/deployment/adapter/index.d.ts +1 -1
- package/dist/deployment/adapter/prepare/context.d.ts +7 -0
- package/dist/deployment/index.d.ts +1 -1
- package/dist/execution/identity/authentication.d.ts +2 -1
- package/dist/execution/identity/authentication.js +3 -3
- package/dist/execution/identity/exchange.d.ts +2 -3
- package/dist/execution/identity/exchange.js +6 -2
- package/dist/execution/identity/identity.js +4 -1
- package/dist/execution/identity/jwks.d.ts +9 -1
- package/dist/execution/identity/jwks.js +126 -2
- package/dist/tooling/cli/arguments.d.ts +0 -1
- package/dist/tooling/cli/arguments.js +41 -13
- package/dist/tooling/cli/bun.d.ts +1 -0
- package/dist/tooling/cli/bun.js +7 -0
- package/dist/tooling/cli/development/develop.d.ts +8 -0
- package/dist/tooling/cli/development/develop.js +284 -0
- package/dist/tooling/cli/development/lifecycle.d.ts +9 -0
- package/dist/tooling/cli/development/lifecycle.js +39 -0
- package/dist/tooling/cli/development/project-lock.d.ts +17 -0
- package/dist/tooling/cli/development/project-lock.js +193 -0
- package/dist/tooling/cli/development/rebuild.d.ts +13 -0
- package/dist/tooling/cli/development/rebuild.js +58 -0
- package/dist/tooling/cli/development/source-watch.d.ts +14 -0
- package/dist/tooling/cli/development/source-watch.js +92 -0
- package/dist/tooling/cli/orchestrate.js +23 -64
- package/dist/tooling/linter/implementations/source/global.js +4 -2
- package/dist/tooling/linter/implementations/source/views.js +23 -0
- package/dist/tooling/linter/policy/generated.js +2 -2
- package/dist/tooling/linter/requirements/registry.js +1 -0
- package/package.json +1 -1
- package/dist/deployment/adapter/watch/handle.d.ts +0 -5
- package/dist/deployment/adapter/watch/index.d.ts +0 -2
- /package/dist/deployment/adapter/{watch → development}/context.js +0 -0
- /package/dist/deployment/adapter/{watch → development}/index.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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', '
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { PreparedArtifact } from '../prepare/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
8
6
|
}
|
|
@@ -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 './
|
|
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,
|
|
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.
|
|
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,
|
|
107
|
+
async function establishCaller(input, audience, verification, execution, policy) {
|
|
108
108
|
requireLive(execution);
|
|
109
109
|
try {
|
|
110
|
-
const context = await verifyCredential(input, audience,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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 === '--
|
|
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'
|
|
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'
|
|
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
|
-
|
|
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;
|
package/dist/tooling/cli/bun.js
CHANGED
|
@@ -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>;
|