@astrale-os/kernel-protocol 0.5.0-beta.54 → 0.5.0-beta.56

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.
@@ -12,7 +12,7 @@ export { admitTrace, MAXIMUM_TRACEPARENT_BYTES, MAXIMUM_TRACESTATE_BYTES, MAXIMU
12
12
  export { acceptRouting, type Refresh, type Routing, type Via } from './routing.js';
13
13
  export { acceptSchemaTarget, type SchemaTarget } from './schema.js';
14
14
  export { decodeResponse, type ErrorResponse, type Response, type ResponseContext, type SuccessResponse, } from './response.js';
15
- export { SCHEMA_REVISION_MISMATCH, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, type SchemaRevisionMismatchDetails, type SchemaRevisionMismatchReason, type ServiceCandidateInvalidDetails, type ServiceCandidateInvalidReason, type ServiceErrorPayload, type ServiceReason, type ServiceTrustRequiredDetails, type ServiceTrustRequiredReason, type ServiceUnavailableDetails, type ServiceUnavailableReason, } from './service-reason.js';
15
+ export { CALLABLE_FINGERPRINT_MISMATCH, SCHEMA_REVISION_MISMATCH, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, type CallableFingerprintMismatchDetails, type CallableFingerprintMismatchReason, type SchemaRevisionMismatchDetails, type SchemaRevisionMismatchReason, type ServiceCandidateInvalidDetails, type ServiceCandidateInvalidReason, type ServiceErrorPayload, type ServiceReason, type ServiceTrustRequiredDetails, type ServiceTrustRequiredReason, type ServiceUnavailableDetails, type ServiceUnavailableReason, } from './service-reason.js';
16
16
  export { decodeStale, stale, type Stale } from './stale-route.js';
17
17
  export { decodeStreamFrame, type StreamEndFrame, type StreamErrorFrame, type StreamFrame, type StreamItemFrame, type StreamStartFrame, } from './stream.js';
18
18
  export { Target, targetPath, acceptCall, type Call } from './target.js';
@@ -12,7 +12,7 @@ export { admitTrace, MAXIMUM_TRACEPARENT_BYTES, MAXIMUM_TRACESTATE_BYTES, MAXIMU
12
12
  export { acceptRouting } from './routing.js';
13
13
  export { acceptSchemaTarget } from './schema.js';
14
14
  export { decodeResponse, } from './response.js';
15
- export { SCHEMA_REVISION_MISMATCH, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, } from './service-reason.js';
15
+ export { CALLABLE_FINGERPRINT_MISMATCH, SCHEMA_REVISION_MISMATCH, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, } from './service-reason.js';
16
16
  export { decodeStale, stale } from './stale-route.js';
17
17
  export { decodeStreamFrame, } from './stream.js';
18
18
  export { Target, targetPath, acceptCall } from './target.js';
@@ -1,9 +1,16 @@
1
1
  import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
2
2
  import type { Sha256Digest } from '../publication/index.js';
3
- /** Source-Kernel precondition for the active Schema Registration selected by one invocation. */
3
+ /**
4
+ * Source-Kernel precondition for one invocation: the exact installed artifact target, the Schema
5
+ * revision, or the fingerprint of the callable meaning the static consumer compiled against.
6
+ */
4
7
  export type SchemaTarget = {
5
8
  readonly target: Sha256Digest;
6
- } | {
9
+ }
10
+ /** @deprecated Pin the callable meaning with `fingerprint`; revision pinning will be removed. */
11
+ | {
7
12
  readonly revision: Revision;
13
+ } | {
14
+ readonly fingerprint: Sha256Digest;
8
15
  };
9
16
  export declare function acceptSchemaTarget(input: unknown): SchemaTarget;
@@ -1,17 +1,18 @@
1
1
  import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
2
2
  import { digest, exactFields, invalid, portableObject } from './admission.js';
3
- const FIELDS = new Set(['target', 'revision']);
3
+ const FIELDS = new Set(['target', 'revision', 'fingerprint']);
4
4
  export function acceptSchemaTarget(input) {
5
5
  const object = portableObject(input, 'schema target');
6
6
  exactFields(object, FIELDS, 'schema target');
7
- const hasTarget = Object.hasOwn(object, 'target');
8
- const hasRevision = Object.hasOwn(object, 'revision');
9
- if (hasTarget === hasRevision) {
10
- invalid('schema target must contain exactly one of target or revision');
7
+ if ([...FIELDS].filter((field) => Object.hasOwn(object, field)).length !== 1) {
8
+ invalid('schema target must contain exactly one of target, revision or fingerprint');
11
9
  }
12
- if (hasTarget) {
10
+ if (Object.hasOwn(object, 'target')) {
13
11
  return Object.freeze({ target: digest(object.target, 'schema target.target') });
14
12
  }
13
+ if (Object.hasOwn(object, 'fingerprint')) {
14
+ return Object.freeze({ fingerprint: digest(object.fingerprint, 'schema target.fingerprint') });
15
+ }
15
16
  if (typeof object.revision !== 'string' || !patterns.revision.test(object.revision)) {
16
17
  invalid('schema target.revision must be a Schema revision');
17
18
  }
@@ -1,18 +1,28 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
2
  import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
3
3
  import type { Object as ValueObject } from '@astrale-os/kernel-dsl/value';
4
- import { type Key } from '@astrale-os/kernel-dsl/v1/addressing';
4
+ import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
5
5
  import type { ErrorPayload, ErrorReason } from '../errors/index.js';
6
+ import type { Sha256Digest } from '../publication/index.js';
6
7
  import { CODES } from '../errors/index.js';
8
+ /** @deprecated Emitted only for the deprecated revision pin; fingerprint pins report CALLABLE_FINGERPRINT_MISMATCH. */
7
9
  export declare const SCHEMA_REVISION_MISMATCH: 'SCHEMA_REVISION_MISMATCH';
10
+ export declare const CALLABLE_FINGERPRINT_MISMATCH: 'CALLABLE_FINGERPRINT_MISMATCH';
8
11
  export declare const SERVICE_TRUST_REQUIRED: 'SERVICE_TRUST_REQUIRED';
9
12
  export declare const SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE';
10
13
  export declare const SERVICE_CANDIDATE_INVALID: 'SERVICE_CANDIDATE_INVALID';
14
+ /** @deprecated Detail shape of the deprecated revision pin conflict. */
11
15
  export type SchemaRevisionMismatchDetails = ValueObject & {
12
16
  readonly origin: Key.Origin;
13
17
  readonly expectedRevision: Revision;
14
18
  readonly installedRevision: Revision;
15
19
  };
20
+ export type CallableFingerprintMismatchDetails = ValueObject & {
21
+ readonly origin: Key.Origin;
22
+ readonly callable: Key.Callable;
23
+ readonly expectedFingerprint: Sha256Digest;
24
+ readonly installedFingerprint: Sha256Digest;
25
+ };
16
26
  export type ServiceTrustRequiredDetails = ValueObject & {
17
27
  readonly origin: Key.Origin;
18
28
  readonly installedIssuer: IssuerId;
@@ -25,14 +35,16 @@ export type ServiceUnavailableDetails = ValueObject & {
25
35
  export type ServiceCandidateInvalidDetails = ValueObject & {
26
36
  readonly origin: Key.Origin;
27
37
  };
38
+ /** @deprecated Detail shape of the deprecated revision pin conflict. */
28
39
  export type SchemaRevisionMismatchReason = ErrorReason<typeof SCHEMA_REVISION_MISMATCH, SchemaRevisionMismatchDetails>;
40
+ export type CallableFingerprintMismatchReason = ErrorReason<typeof CALLABLE_FINGERPRINT_MISMATCH, CallableFingerprintMismatchDetails>;
29
41
  export type ServiceTrustRequiredReason = ErrorReason<typeof SERVICE_TRUST_REQUIRED, ServiceTrustRequiredDetails>;
30
42
  export type ServiceUnavailableReason = ErrorReason<typeof SERVICE_UNAVAILABLE, ServiceUnavailableDetails>;
31
43
  export type ServiceCandidateInvalidReason = ErrorReason<typeof SERVICE_CANDIDATE_INVALID, ServiceCandidateInvalidDetails>;
32
- export type ServiceReason = SchemaRevisionMismatchReason | ServiceTrustRequiredReason | ServiceUnavailableReason | ServiceCandidateInvalidReason;
44
+ export type ServiceReason = SchemaRevisionMismatchReason | CallableFingerprintMismatchReason | ServiceTrustRequiredReason | ServiceUnavailableReason | ServiceCandidateInvalidReason;
33
45
  export type ServiceErrorPayload = (Omit<ErrorPayload, 'code' | 'data'> & {
34
46
  readonly code: typeof CODES.CONFLICT;
35
- readonly data: SchemaRevisionMismatchReason | ServiceTrustRequiredReason;
47
+ readonly data: SchemaRevisionMismatchReason | CallableFingerprintMismatchReason | ServiceTrustRequiredReason;
36
48
  }) | (Omit<ErrorPayload, 'code' | 'data'> & {
37
49
  readonly code: typeof CODES.BACKEND_UNAVAILABLE;
38
50
  readonly data: ServiceUnavailableReason | ServiceCandidateInvalidReason;
@@ -1,8 +1,10 @@
1
1
  import { AuthValueError } from '@astrale-os/kernel-core/auth/error';
2
2
  import { accept as acceptIssuer } from '@astrale-os/kernel-core/auth/issuer';
3
- import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
3
+ import { Key, patterns } from '@astrale-os/kernel-dsl/v1/addressing';
4
4
  import { CODES, reasonOf } from '../errors/index.js';
5
+ /** @deprecated Emitted only for the deprecated revision pin; fingerprint pins report CALLABLE_FINGERPRINT_MISMATCH. */
5
6
  export const SCHEMA_REVISION_MISMATCH = 'SCHEMA_REVISION_MISMATCH';
7
+ export const CALLABLE_FINGERPRINT_MISMATCH = 'CALLABLE_FINGERPRINT_MISMATCH';
6
8
  export const SERVICE_TRUST_REQUIRED = 'SERVICE_TRUST_REQUIRED';
7
9
  export const SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE';
8
10
  export const SERVICE_CANDIDATE_INVALID = 'SERVICE_CANDIDATE_INVALID';
@@ -16,6 +18,10 @@ export function serviceErrorOf(input) {
16
18
  return input.code === CODES.CONFLICT && schemaRevisionMismatchDetails(reason.details)
17
19
  ? input
18
20
  : undefined;
21
+ case CALLABLE_FINGERPRINT_MISMATCH:
22
+ return input.code === CODES.CONFLICT && callableFingerprintMismatchDetails(reason.details)
23
+ ? input
24
+ : undefined;
19
25
  case SERVICE_TRUST_REQUIRED:
20
26
  return input.code === CODES.CONFLICT && serviceTrustDetails(reason.details)
21
27
  ? input
@@ -38,6 +44,15 @@ function schemaRevisionMismatchDetails(input) {
38
44
  isRevision(input.expectedRevision) &&
39
45
  isRevision(input.installedRevision));
40
46
  }
47
+ function callableFingerprintMismatchDetails(input) {
48
+ return (exact(input, ['origin', 'callable', 'expectedFingerprint', 'installedFingerprint']) &&
49
+ isOrigin(input.origin) &&
50
+ typeof input.callable === 'string' &&
51
+ Key.is(input.callable) &&
52
+ Key.origin(input.callable) === input.origin &&
53
+ isDigest(input.expectedFingerprint) &&
54
+ isDigest(input.installedFingerprint));
55
+ }
41
56
  function serviceTrustDetails(input) {
42
57
  return (exact(input, ['origin', 'installedIssuer', 'candidateIssuer']) &&
43
58
  isOrigin(input.origin) &&
@@ -60,6 +75,9 @@ function isOrigin(input) {
60
75
  function isRevision(input) {
61
76
  return typeof input === 'string' && patterns.revision.test(input);
62
77
  }
78
+ function isDigest(input) {
79
+ return typeof input === 'string' && /^sha256:[0-9a-f]{64}$/u.test(input);
80
+ }
63
81
  function isIssuer(input) {
64
82
  if (typeof input !== 'string')
65
83
  return false;
@@ -3,4 +3,4 @@ import type { Publication } from '../publication/publication.js';
3
3
  import type { Plan } from './plan.js';
4
4
  import type { Table } from './table.js';
5
5
  /** Bind one Plan to an exact placed Publication and matching resolved Domain. */
6
- export declare function bind(plan: Plan, deployed: Publication, domain: Domain): Table;
6
+ export declare function bind(plan: Plan, deployed: Publication, domain: Domain, publicUrl?: string): Table;
@@ -4,15 +4,14 @@ import { fail } from './error.js';
4
4
  import { place } from './placement.js';
5
5
  import { rejectReserved } from './reservation.js';
6
6
  /** Bind one Plan to an exact placed Publication and matching resolved Domain. */
7
- export function bind(plan, deployed, domain) {
7
+ export function bind(plan, deployed, domain, publicUrl = deployed.identity.issuer) {
8
8
  if (plan.origin !== deployed.origin ||
9
9
  plan.origin !== domain.origin ||
10
10
  plan.revision !== deployed.schema.revision ||
11
11
  plan.revision !== domain.revision) {
12
12
  fail('NATIVE_ROUTE_BINDING_INVALID', 'Route Plan, Publication, and Domain identity differ.');
13
13
  }
14
- if (json.serialize(place(plan, deployed.identity.issuer)) !==
15
- json.serialize(deployed.routes)) {
14
+ if (json.serialize(place(plan, publicUrl)) !== json.serialize(deployed.routes)) {
16
15
  fail('NATIVE_ROUTE_BINDING_INVALID', 'Publication routes differ from the placed Route Plan.');
17
16
  }
18
17
  const entries = plan.entries.map((entry, index) => {
@@ -1,6 +1,8 @@
1
1
  import type { introspection } from '@astrale-os/kernel-core/schema';
2
- import type { DomainInfo, Limits, ResultFor } from './result.js';
2
+ import type { DomainDependencies, DomainInfo, Limits, ResultFor } from './result.js';
3
3
  export declare const MEDIA_TYPE: 'application/vnd.astrale.schema-introspection.v2+msgpack';
4
4
  export declare function encode<Input extends introspection.Introspection>(input: Input, result: ResultFor<Input>): Promise<Uint8Array>;
5
5
  export declare function decode<Input extends introspection.Introspection>(input: Input, bytes: Uint8Array, limits: Limits): Promise<ResultFor<Input>>;
6
+ /** Admit dependency bindings: origin-ordered, one per origin, each label agreeing with its revisions. */
7
+ export declare function acceptDomainDependencies(input: unknown): DomainDependencies;
6
8
  export declare function acceptDomainInfo(input: unknown): DomainInfo;
@@ -11,7 +11,9 @@ export async function encode(input, result) {
11
11
  const request = admitRequest(input);
12
12
  const wire = request.kind === 'bundle'
13
13
  ? encodeDomainBundle(result)
14
- : encodeDomainInfo(result);
14
+ : request.kind === 'dependencies'
15
+ ? encodeDomainDependencies(result)
16
+ : encodeDomainInfo(result);
15
17
  proveRequestedOrigin(request, result);
16
18
  // Non-literal specifiers keep msgpackr's Node-typed declarations out of consumers (see codecs/msgpack.ts).
17
19
  const { pack } = await import('msgpackr/pack');
@@ -40,17 +42,66 @@ export async function decode(input, bytes, limits) {
40
42
  }
41
43
  const result = (request.kind === 'bundle'
42
44
  ? decodeDomainBundle(decoded, limits.maximumBundleBytes)
43
- : acceptDomainInfo(decoded));
45
+ : request.kind === 'dependencies'
46
+ ? acceptDomainDependencies(decoded)
47
+ : acceptDomainInfo(decoded));
44
48
  proveRequestedOrigin(request, result);
45
49
  return result;
46
50
  }
47
51
  function proveRequestedOrigin(request, result) {
48
- const origin = request.kind === 'bundle'
49
- ? result.domain.origin
50
- : result.origin;
52
+ const origin = request.kind === 'inspect'
53
+ ? result.origin
54
+ : result.domain.origin;
51
55
  if (origin !== request.origin)
52
56
  invalid('Schema introspection evidence does not match the requested origin.');
53
57
  }
58
+ function encodeDomainDependencies(input) {
59
+ const admitted = acceptDomainDependencies(input);
60
+ return Object.freeze({
61
+ domain: encodeDomainInfo(admitted.domain),
62
+ dependencies: admitted.dependencies.map((binding) => ({ ...binding })),
63
+ });
64
+ }
65
+ /** Admit dependency bindings: origin-ordered, one per origin, each label agreeing with its revisions. */
66
+ export function acceptDomainDependencies(input) {
67
+ const value = record(input, 'Domain dependencies result');
68
+ exact(value, ['dependencies', 'domain'], 'Domain dependencies result');
69
+ const domain = acceptDomainInfo(value.domain);
70
+ if (!Array.isArray(value.dependencies))
71
+ invalid('Domain dependency bindings are invalid.');
72
+ const dependencies = value.dependencies.map(acceptDependencyBinding);
73
+ for (const [index, binding] of dependencies.entries()) {
74
+ const previous = dependencies[index - 1];
75
+ if (previous !== undefined && previous.origin >= binding.origin) {
76
+ invalid('Domain dependency bindings must be origin-ordered and unique.');
77
+ }
78
+ if (binding.origin === domain.origin)
79
+ invalid('Domain dependency bindings cannot name the Domain.');
80
+ }
81
+ return Object.freeze({ domain, dependencies: Object.freeze(dependencies) });
82
+ }
83
+ function acceptDependencyBinding(input) {
84
+ const value = record(input, 'Domain dependency binding');
85
+ exact(value, ['active', 'binding', 'origin', 'pinned'], 'Domain dependency binding');
86
+ if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
87
+ invalid('Domain dependency binding origin is invalid.');
88
+ }
89
+ if (typeof value.pinned !== 'string' || !patterns.revision.test(value.pinned)) {
90
+ invalid('Domain dependency binding pinned revision is invalid.');
91
+ }
92
+ if (typeof value.active !== 'string' || !patterns.revision.test(value.active)) {
93
+ invalid('Domain dependency binding active revision is invalid.');
94
+ }
95
+ const expected = value.pinned === value.active ? 'exact' : 'retained';
96
+ if (value.binding !== expected)
97
+ invalid('Domain dependency binding label is invalid.');
98
+ return Object.freeze({
99
+ origin: value.origin,
100
+ pinned: value.pinned,
101
+ active: value.active,
102
+ binding: expected,
103
+ });
104
+ }
54
105
  function encodeDomainBundle(input) {
55
106
  exact(input, ['bundle', 'domain'], 'Domain Bundle result');
56
107
  const admittedDomain = acceptDomainInfo(input.domain);
@@ -172,7 +223,7 @@ function capabilities(input) {
172
223
  function admitRequest(input) {
173
224
  const value = record(input, 'Schema introspection request');
174
225
  exact(value, ['kind', 'origin'], 'Schema introspection request');
175
- if (value.kind !== 'inspect' && value.kind !== 'bundle') {
226
+ if (value.kind !== 'inspect' && value.kind !== 'bundle' && value.kind !== 'dependencies') {
176
227
  invalid('Schema introspection kind is invalid.');
177
228
  }
178
229
  if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
@@ -1,2 +1,2 @@
1
- export { MEDIA_TYPE, acceptDomainInfo, decode, encode } from './codec.js';
2
- export type { Digest, DomainBundle, DomainInfo, Limits, ResultFor } from './result.js';
1
+ export { MEDIA_TYPE, acceptDomainDependencies, acceptDomainInfo, decode, encode } from './codec.js';
2
+ export type { DependencyBinding, Digest, DomainBundle, DomainDependencies, DomainInfo, Limits, ResultFor, } from './result.js';
@@ -1 +1 @@
1
- export { MEDIA_TYPE, acceptDomainInfo, decode, encode } from './codec.js';
1
+ export { MEDIA_TYPE, acceptDomainDependencies, acceptDomainInfo, decode, encode } from './codec.js';
@@ -20,9 +20,27 @@ export interface DomainBundle {
20
20
  readonly domain: DomainInfo;
21
21
  readonly bundle: bundle.Bundle;
22
22
  }
23
+ /**
24
+ * How one ready Domain generation binds one dependency of its exact closure: `exact` when the
25
+ * pinned revision is the one the Kernel serves, `retained` when the Kernel serves another revision
26
+ * and keeps the pinned one as inactive context admitted by unchanged meaning.
27
+ */
28
+ export interface DependencyBinding {
29
+ readonly origin: Key.Origin;
30
+ readonly pinned: Revision;
31
+ readonly active: Revision;
32
+ readonly binding: 'exact' | 'retained';
33
+ }
34
+ /** The origin-ordered dependency bindings observed coherently with a ready Domain generation. */
35
+ export interface DomainDependencies {
36
+ readonly domain: DomainInfo;
37
+ readonly dependencies: readonly DependencyBinding[];
38
+ }
23
39
  export type ResultFor<Input extends introspection.Introspection> = Input extends {
24
40
  readonly kind: 'bundle';
25
- } ? DomainBundle : DomainInfo;
41
+ } ? DomainBundle : Input extends {
42
+ readonly kind: 'dependencies';
43
+ } ? DomainDependencies : DomainInfo;
26
44
  export interface Limits {
27
45
  readonly maximumPublicationBytes: number;
28
46
  readonly maximumBundleBytes: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-protocol",
3
- "version": "0.5.0-beta.54",
3
+ "version": "0.5.0-beta.56",
4
4
  "description": "Portable Astrale invocation, publication, installation, routing, and issuer protocols",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -137,8 +137,8 @@
137
137
  "access": "public"
138
138
  },
139
139
  "dependencies": {
140
- "@astrale-os/kernel-core": "0.9.0-beta.48",
141
- "@astrale-os/kernel-dsl": "0.2.0-beta.33",
140
+ "@astrale-os/kernel-core": "0.9.0-beta.50",
141
+ "@astrale-os/kernel-dsl": "0.2.0-beta.35",
142
142
  "@noble/hashes": "2.3.0"
143
143
  },
144
144
  "devDependencies": {