@astrale-os/sdk 0.6.0-beta.21 → 0.6.0-beta.23

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 (58) hide show
  1. package/README.md +0 -4
  2. package/dist/application/workflow/durable.d.ts +12 -8
  3. package/dist/deployment/adapter/adapter.d.ts +16 -5
  4. package/dist/deployment/adapter/define.d.ts +1 -1
  5. package/dist/deployment/adapter/define.js +40 -1
  6. package/dist/deployment/adapter/release/command.d.ts +33 -0
  7. package/dist/deployment/adapter/release/command.js +1 -0
  8. package/dist/deployment/adapter/release/context.d.ts +3 -4
  9. package/dist/deployment/adapter/release/index.d.ts +1 -0
  10. package/dist/deployment/adapter/release/list.d.ts +11 -10
  11. package/dist/deployment/address/configuration.d.ts +12 -0
  12. package/dist/deployment/address/configuration.js +11 -0
  13. package/dist/deployment/address/index.d.ts +3 -3
  14. package/dist/deployment/address/index.js +2 -3
  15. package/dist/deployment/address/label.d.ts +5 -5
  16. package/dist/deployment/address/label.js +5 -5
  17. package/dist/deployment/address/record.d.ts +21 -0
  18. package/dist/deployment/address/record.js +34 -0
  19. package/dist/deployment/address/summary.d.ts +17 -40
  20. package/dist/deployment/address/summary.js +1 -117
  21. package/dist/deployment/index.d.ts +1 -1
  22. package/dist/deployment/verify/index.d.ts +1 -1
  23. package/dist/deployment/verify/index.js +1 -1
  24. package/dist/deployment/verify/readiness/index.d.ts +7 -0
  25. package/dist/deployment/verify/readiness/index.js +17 -0
  26. package/dist/deployment/worker/address.d.ts +26 -0
  27. package/dist/deployment/worker/address.js +43 -0
  28. package/dist/deployment/worker/build.d.ts +60 -0
  29. package/dist/deployment/worker/build.js +172 -0
  30. package/dist/deployment/worker/configuration.d.ts +73 -0
  31. package/dist/deployment/worker/configuration.js +165 -0
  32. package/dist/deployment/worker/content.d.ts +38 -0
  33. package/dist/deployment/worker/content.js +169 -0
  34. package/dist/deployment/worker/index.d.ts +29 -0
  35. package/dist/deployment/worker/index.js +29 -0
  36. package/dist/deployment/worker/limits.d.ts +30 -0
  37. package/dist/deployment/worker/limits.js +30 -0
  38. package/dist/deployment/worker/record.d.ts +29 -0
  39. package/dist/deployment/worker/record.js +69 -0
  40. package/dist/deployment/worker/request.d.ts +118 -0
  41. package/dist/deployment/worker/request.js +280 -0
  42. package/dist/deployment/worker/response.d.ts +104 -0
  43. package/dist/deployment/worker/response.js +213 -0
  44. package/dist/execution/workflows/run.js +18 -6
  45. package/dist/tooling/cli/adapter-command.d.ts +35 -0
  46. package/dist/tooling/cli/adapter-command.js +66 -0
  47. package/dist/tooling/cli/arguments.d.ts +7 -1
  48. package/dist/tooling/cli/arguments.js +53 -1
  49. package/dist/tooling/cli/help.js +11 -8
  50. package/dist/tooling/cli/immutable-deployment.d.ts +2 -2
  51. package/dist/tooling/cli/index.d.ts +4 -2
  52. package/dist/tooling/cli/index.js +3 -2
  53. package/dist/tooling/cli/list.d.ts +5 -5
  54. package/dist/tooling/cli/list.js +16 -14
  55. package/dist/tooling/cli/orchestrate.js +18 -0
  56. package/dist/tooling/cli/publish/index.d.ts +4 -4
  57. package/dist/tooling/cli/publish/index.js +8 -9
  58. package/package.json +5 -1
@@ -1,117 +1 @@
1
- import { json } from '@astrale-os/kernel-dsl/value';
2
- import { deploymentUrl, parseDeploymentLabel } from './label.js';
3
- /** The owner every summary of a platform namespace names: the platform holds all of its lines. */
4
- export const PLATFORM_OWNER = 'platform';
5
- const REQUIRED = [
6
- 'id',
7
- 'url',
8
- 'label',
9
- 'line',
10
- 'owner',
11
- 'state',
12
- 'retention',
13
- 'createdAt',
14
- 'signingKeyId',
15
- ];
16
- const RETIREMENT = ['retiredAt', 'retirement'];
17
- const STATES = ['active', 'retiring', 'retired'];
18
- const RETENTIONS = ['preview', 'published'];
19
- /** An instant as `Date.prototype.toISOString` writes it, in UTC. */
20
- const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
21
- /** An RFC 7638 JWK thumbprint: the base64url SHA-256 of the public key, 43 characters. */
22
- const KEY_ID = /^[A-Za-z0-9_-]{43}$/u;
23
- /**
24
- * Admit one platform deployment summary exactly. Any member outside its shape is refused, a
25
- * provider version id or a clock among them; so is a summary whose id, line or URL is not its
26
- * label's, whose owner is not the platform, or whose state and retirement disagree. A reader that
27
- * meets a refused summary lists that label apart, never acting on it.
28
- */
29
- export function acceptPlatformDeploymentSummary(input) {
30
- if (input === null || typeof input !== 'object' || Array.isArray(input))
31
- invalid('members');
32
- const value = input;
33
- const keys = Reflect.ownKeys(value);
34
- const retired = RETIREMENT.some((key) => Object.hasOwn(value, key));
35
- const expected = retired ? [...REQUIRED, ...RETIREMENT] : REQUIRED;
36
- if (keys.length !== expected.length || expected.some((key) => !Object.hasOwn(value, key))) {
37
- invalid('members');
38
- }
39
- const label = text(value.label, 'label');
40
- let line;
41
- try {
42
- line = parseDeploymentLabel(label).line;
43
- }
44
- catch {
45
- invalid('label');
46
- }
47
- if (value.id !== label)
48
- invalid('id');
49
- if (value.line !== line)
50
- invalid('line');
51
- const url = text(value.url, 'url');
52
- if (!sameDeploymentUrl(url, label))
53
- invalid('url');
54
- if (value.owner !== PLATFORM_OWNER)
55
- invalid('owner');
56
- const state = value.state;
57
- if (typeof state !== 'string' || !STATES.includes(state))
58
- invalid('state');
59
- if (retired !== (state !== 'active'))
60
- invalid('state');
61
- const retention = value.retention;
62
- if (typeof retention !== 'string' || !RETENTIONS.includes(retention))
63
- invalid('retention');
64
- const createdAt = instant(value.createdAt, 'createdAt');
65
- const signingKeyId = text(value.signingKeyId, 'signingKeyId');
66
- if (!KEY_ID.test(signingKeyId))
67
- invalid('signingKeyId');
68
- if (retired && value.retirement !== 'withdrawn')
69
- invalid('retirement');
70
- return Object.freeze({
71
- id: label,
72
- url,
73
- label,
74
- line,
75
- owner: PLATFORM_OWNER,
76
- state: state,
77
- retention: retention,
78
- createdAt,
79
- signingKeyId,
80
- ...(retired
81
- ? { retiredAt: instant(value.retiredAt, 'retiredAt'), retirement: 'withdrawn' }
82
- : {}),
83
- });
84
- }
85
- /**
86
- * The canonical JSON (RFC 8785) of one admitted platform summary, as `summary:<label>` holds it:
87
- * one summary has one text, so a writer that reads it back compares texts.
88
- */
89
- export function encodePlatformDeploymentSummary(summary) {
90
- return json.serialize(acceptPlatformDeploymentSummary(summary));
91
- }
92
- /** Whether `url` is exactly `https://<label>.<routing domain>` for some routing domain. */
93
- function sameDeploymentUrl(url, label) {
94
- const prefix = `https://${label}.`;
95
- if (!url.startsWith(prefix))
96
- return false;
97
- try {
98
- return deploymentUrl(label, url.slice(prefix.length)) === url;
99
- }
100
- catch {
101
- return false;
102
- }
103
- }
104
- function text(input, member) {
105
- if (typeof input !== 'string' || input.length === 0)
106
- invalid(member);
107
- return input;
108
- }
109
- function instant(input, member) {
110
- const value = text(input, member);
111
- if (!INSTANT.test(value) || new Date(value).toISOString() !== value)
112
- invalid(member);
113
- return value;
114
- }
115
- function invalid(member) {
116
- throw new TypeError(`Platform deployment summary has invalid ${member}.`);
117
- }
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  export { defineAdapter, defineDeployment, isAdapter } from './adapter/index.js';
2
- export type { Adapter, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
2
+ export type { Adapter, AdapterCommand, AdapterCommandContext, AdapterCommandResult, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
3
3
  export { compile, isBuild } from './build/index.js';
4
4
  export type { Build, BuildBundle, BuildDigest, BuildManifestV1, BuildSchema, } from './build/index.js';
5
5
  export { deploy } from './deploy.js';
@@ -2,4 +2,4 @@ export { preflight as verifyPreflight } from './preflight/index.js';
2
2
  export { isPreparedArtifact } from './preflight/index.js';
3
3
  export { bundle as verifyBundle, isBundle, uploadedBuild as verifyUploadedBuild, } from './preflight/index.js';
4
4
  export type { BundleInput, PreflightInput } from './preflight/index.js';
5
- export { readiness as verifyReadiness, verifyDeployment } from './readiness/index.js';
5
+ export { readiness as verifyReadiness, readinessPaths, verifyDeployment, } from './readiness/index.js';
@@ -1,4 +1,4 @@
1
1
  export { preflight as verifyPreflight } from './preflight/index.js';
2
2
  export { isPreparedArtifact } from './preflight/index.js';
3
3
  export { bundle as verifyBundle, isBundle, uploadedBuild as verifyUploadedBuild, } from './preflight/index.js';
4
- export { readiness as verifyReadiness, verifyDeployment } from './readiness/index.js';
4
+ export { readiness as verifyReadiness, readinessPaths, verifyDeployment, } from './readiness/index.js';
@@ -11,4 +11,11 @@ export { verifyRelease } from './release.js';
11
11
  * deployment's `release.json` (release digest and build digest), or a stable target's
12
12
  * `domain.json` (deprecated, `legacy/`).
13
13
  */
14
+ /**
15
+ * The local paths `readiness` reads of one immutable deployment, each answering 200 once the
16
+ * deployment serves its release: its `release.json`, its Bundle and its issuer documents (the
17
+ * OpenID configuration and its key set). A host that probes paths before it activates a
18
+ * deployment probes these.
19
+ */
20
+ export declare function readinessPaths(deployed: Release): readonly string[];
14
21
  export declare function readiness(release: Release | legacy.Release, options?: ReadinessOptions): Promise<void>;
@@ -1,3 +1,5 @@
1
+ import { issuer } from '@astrale-os/kernel-protocol';
2
+ import * as release from '@astrale-os/kernel-protocol/release';
1
3
  import { verifyIdentity } from './identity.js';
2
4
  import { verifyPublication } from './legacy/index.js';
3
5
  import { verifyRelease } from './release.js';
@@ -10,6 +12,21 @@ export { verifyRelease } from './release.js';
10
12
  * deployment's `release.json` (release digest and build digest), or a stable target's
11
13
  * `domain.json` (deprecated, `legacy/`).
12
14
  */
15
+ /**
16
+ * The local paths `readiness` reads of one immutable deployment, each answering 200 once the
17
+ * deployment serves its release: its `release.json`, its Bundle and its issuer documents (the
18
+ * OpenID configuration and its key set). A host that probes paths before it activates a
19
+ * deployment probes these.
20
+ */
21
+ export function readinessPaths(deployed) {
22
+ const { configuration, keys } = issuer.paths(deployed.url);
23
+ return Object.freeze([
24
+ new URL(release.url(deployed.url)).pathname,
25
+ new URL(deployed.document.schema.bundle.href).pathname,
26
+ configuration,
27
+ keys,
28
+ ]);
29
+ }
13
30
  export async function readiness(release, options = {}) {
14
31
  if (release.kind === 'release') {
15
32
  await verifyRelease(release, options);
@@ -0,0 +1,26 @@
1
+ import type { Digest } from '@astrale-os/kernel-protocol/artifact';
2
+ /**
3
+ * The name of a project a Worker host keeps Services for: lower-case letters, digits, `.`, `_`
4
+ * and `-`, starting and ending with a letter or a digit, at most 253 characters. A Domain origin is
5
+ * one (a Domain deployer names its project by its origin).
6
+ */
7
+ export declare const PROJECT_NAME: RegExp;
8
+ /**
9
+ * The name of the Service of one project in one environment: the first 80 bits of
10
+ * `sha256(utf8(project) ‖ 0x00 ‖ utf8(environment))` in lower-case base32, 16 characters. It is
11
+ * opaque, so a Service's URL says nothing of whose code it serves, and it is the line fingerprint
12
+ * of `@astrale-os/sdk/deployment/address` (`lineFingerprint`) when the project is a Domain origin:
13
+ * a Domain's Service keeps its name.
14
+ */
15
+ export declare function serviceName(project: string, environment: string): string;
16
+ /**
17
+ * The label of one Worker deployment, `<service>-<content>`: its Service's name, then the first
18
+ * 80 bits of `sha256(utf8(buildDigest) ‖ 0x00 ‖ utf8(configurationDigest))` in lower-case base32.
19
+ * The same code and configuration in the same Service always give the same label, so a redeploy
20
+ * of a release reuses its deployment; anything else gives another.
21
+ */
22
+ export declare function workerDeploymentLabel(input: {
23
+ readonly service: string;
24
+ readonly buildDigest: Digest;
25
+ readonly configurationDigest: Digest;
26
+ }): string;
@@ -0,0 +1,43 @@
1
+ import { first80 } from '../address/fingerprint.js';
2
+ import { deploymentLabel } from '../address/label.js';
3
+ import { ENVIRONMENT_NAME } from '../environment.js';
4
+ /**
5
+ * The name of a project a Worker host keeps Services for: lower-case letters, digits, `.`, `_`
6
+ * and `-`, starting and ending with a letter or a digit, at most 253 characters. A Domain origin is
7
+ * one (a Domain deployer names its project by its origin).
8
+ */
9
+ export const PROJECT_NAME = /^[a-z0-9](?:[a-z0-9._-]{0,251}[a-z0-9])?$/u;
10
+ /** The longest environment name a Service is named by. */
11
+ const MAXIMUM_ENVIRONMENT_LENGTH = 63;
12
+ /**
13
+ * The name of the Service of one project in one environment: the first 80 bits of
14
+ * `sha256(utf8(project) ‖ 0x00 ‖ utf8(environment))` in lower-case base32, 16 characters. It is
15
+ * opaque, so a Service's URL says nothing of whose code it serves, and it is the line fingerprint
16
+ * of `@astrale-os/sdk/deployment/address` (`lineFingerprint`) when the project is a Domain origin:
17
+ * a Domain's Service keeps its name.
18
+ */
19
+ export function serviceName(project, environment) {
20
+ if (typeof project !== 'string' || !PROJECT_NAME.test(project)) {
21
+ throw new TypeError('Worker project must be a lower-case project name.');
22
+ }
23
+ if (typeof environment !== 'string' ||
24
+ environment.length > MAXIMUM_ENVIRONMENT_LENGTH ||
25
+ !ENVIRONMENT_NAME.test(environment)) {
26
+ throw new TypeError('Worker environment must be a project environment name.');
27
+ }
28
+ return first80(project, environment);
29
+ }
30
+ /**
31
+ * The label of one Worker deployment, `<service>-<content>`: its Service's name, then the first
32
+ * 80 bits of `sha256(utf8(buildDigest) ‖ 0x00 ‖ utf8(configurationDigest))` in lower-case base32.
33
+ * The same code and configuration in the same Service always give the same label, so a redeploy
34
+ * of a release reuses its deployment; anything else gives another.
35
+ */
36
+ export function workerDeploymentLabel(input) {
37
+ for (const digest of [input.buildDigest, input.configurationDigest]) {
38
+ if (typeof digest !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(digest)) {
39
+ throw new TypeError('Worker deployment digests must be sha256 digests.');
40
+ }
41
+ }
42
+ return deploymentLabel(input.service, first80(input.buildDigest, input.configurationDigest));
43
+ }
@@ -0,0 +1,60 @@
1
+ import type { Value } from '@astrale-os/kernel-dsl/value';
2
+ import type { Digest } from '@astrale-os/kernel-protocol/artifact';
3
+ /** How Workers loads one module. */
4
+ export type WorkerModuleKind = 'esm' | 'wasm' | 'text' | 'data';
5
+ /** One module of a Worker, by the name the others import it by. */
6
+ export interface WorkerModule {
7
+ readonly name: string;
8
+ readonly kind: WorkerModuleKind;
9
+ readonly bytes: Uint8Array;
10
+ }
11
+ /** One static asset of a Worker, by the absolute URL path it is served at. */
12
+ export interface WorkerAsset {
13
+ readonly path: string;
14
+ readonly bytes: Uint8Array;
15
+ /** The media type the asset is uploaded with; not part of the build digest. */
16
+ readonly contentType?: string;
17
+ }
18
+ /**
19
+ * The members of a build manifest its deployer declares: at least its `format` and `version`,
20
+ * then whatever provenance the deployer identifies its builds by. A host never reads them: it only
21
+ * digests them with the `entrypoint` and `files` it computes from the uploaded bytes.
22
+ *
23
+ * A generic Worker declares `WORKER_BUILD_MANIFEST`. A Domain deployer declares its
24
+ * `BuildManifestV1` without `entrypoint` and `files` (`format`, `version`, `origin`, `schema`,
25
+ * `bundler`), so the digest is exactly the Domain's build digest.
26
+ */
27
+ export type WorkerBuildManifest = Readonly<Record<string, Value>> & {
28
+ readonly format: string;
29
+ readonly version: number;
30
+ };
31
+ /** The build manifest members of a Worker that declares no provenance of its own. */
32
+ export declare const WORKER_BUILD_MANIFEST: WorkerBuildManifest;
33
+ /** The code of one Worker: its modules, its main module, its static assets and its manifest. */
34
+ export interface WorkerCode {
35
+ readonly manifest: WorkerBuildManifest;
36
+ readonly mainModule: string;
37
+ readonly modules: readonly WorkerModule[];
38
+ readonly assets?: readonly WorkerAsset[];
39
+ }
40
+ /**
41
+ * The build manifest of one Worker: its declared members, its `entrypoint` (the main module) and
42
+ * its `files`, sorted by path (UTF-16 code units), each `{ path, digest, size }`. A module is the
43
+ * file of its own name, a static asset `/<path>` the file `assets/<path>`. A module whose kind is
44
+ * not the one its name implies (`wasm` for a `.wasm` name, `esm` otherwise) also records its
45
+ * `kind`, so a module loaded another way is another build. The code is admitted first.
46
+ */
47
+ export declare function workerBuildManifest(code: WorkerCode): Readonly<Record<string, Value>>;
48
+ /**
49
+ * The build digest of one Worker: `sha256` of the canonical JSON (RFC 8785) of its build manifest.
50
+ * A host binds it as `ASTRALE_BUILD_DIGEST`; with a Domain's declared members it is the Domain's
51
+ * build digest.
52
+ */
53
+ export declare function workerBuildDigest(code: WorkerCode): Digest;
54
+ /**
55
+ * Admit the code of one Worker: declared manifest members (a `format`, a positive `version`, no
56
+ * `entrypoint` nor `files`, at most `manifestSize` bytes of canonical JSON), one to `modules`
57
+ * modules with safe relative names, unique, the main module among them, and at most `assets`
58
+ * assets at unique canonical absolute paths. Sizes are bounded per file and for the whole Worker.
59
+ */
60
+ export declare function acceptWorkerCode(code: WorkerCode): WorkerCode;
@@ -0,0 +1,172 @@
1
+ import { json } from '@astrale-os/kernel-dsl/value';
2
+ import { identify } from '@astrale-os/kernel-protocol/artifact';
3
+ import { WORKER_DEPLOYMENT_LIMITS } from './limits.js';
4
+ /** The build manifest members of a Worker that declares no provenance of its own. */
5
+ export const WORKER_BUILD_MANIFEST = Object.freeze({
6
+ format: 'astrale.worker.build',
7
+ version: 1,
8
+ });
9
+ const MANIFEST_MEDIA_TYPE = 'application/vnd.astrale.worker-build+json;v=1';
10
+ const FILE_MEDIA_TYPE = 'application/octet-stream';
11
+ const MAXIMUM_MODULE_NAME_LENGTH = 256;
12
+ const MAXIMUM_ASSET_PATH_LENGTH = 2_048;
13
+ const KINDS = Object.freeze(['esm', 'wasm', 'text', 'data']);
14
+ /**
15
+ * The build manifest of one Worker: its declared members, its `entrypoint` (the main module) and
16
+ * its `files`, sorted by path (UTF-16 code units), each `{ path, digest, size }`. A module is the
17
+ * file of its own name, a static asset `/<path>` the file `assets/<path>`. A module whose kind is
18
+ * not the one its name implies (`wasm` for a `.wasm` name, `esm` otherwise) also records its
19
+ * `kind`, so a module loaded another way is another build. The code is admitted first.
20
+ */
21
+ export function workerBuildManifest(code) {
22
+ const admitted = acceptWorkerCode(code);
23
+ const files = [
24
+ ...admitted.modules.map((module) => ({
25
+ path: module.name,
26
+ ...identity(module.bytes),
27
+ ...(module.kind === impliedKind(module.name) ? {} : { kind: module.kind }),
28
+ })),
29
+ ...(admitted.assets ?? []).map((asset) => ({
30
+ path: `assets${asset.path}`,
31
+ ...identity(asset.bytes),
32
+ })),
33
+ ].sort((left, right) => compare(left.path, right.path));
34
+ for (let index = 1; index < files.length; index += 1) {
35
+ if (files[index - 1].path === files[index].path) {
36
+ invalid(`the module and the asset file ${files[index].path} collide`);
37
+ }
38
+ }
39
+ return Object.freeze({ ...admitted.manifest, entrypoint: admitted.mainModule, files });
40
+ }
41
+ /**
42
+ * The build digest of one Worker: `sha256` of the canonical JSON (RFC 8785) of its build manifest.
43
+ * A host binds it as `ASTRALE_BUILD_DIGEST`; with a Domain's declared members it is the Domain's
44
+ * build digest.
45
+ */
46
+ export function workerBuildDigest(code) {
47
+ return identify(json.encode(workerBuildManifest(code)), MANIFEST_MEDIA_TYPE).digest;
48
+ }
49
+ /**
50
+ * Admit the code of one Worker: declared manifest members (a `format`, a positive `version`, no
51
+ * `entrypoint` nor `files`, at most `manifestSize` bytes of canonical JSON), one to `modules`
52
+ * modules with safe relative names, unique, the main module among them, and at most `assets`
53
+ * assets at unique canonical absolute paths. Sizes are bounded per file and for the whole Worker.
54
+ */
55
+ export function acceptWorkerCode(code) {
56
+ if (code === null || typeof code !== 'object')
57
+ invalid('it is not an object');
58
+ const manifest = acceptManifest(code.manifest);
59
+ const limits = WORKER_DEPLOYMENT_LIMITS;
60
+ if (!Array.isArray(code.modules) || code.modules.length === 0)
61
+ invalid('it has no module');
62
+ if (code.modules.length > limits.modules)
63
+ invalid('it has too many modules');
64
+ const assets = code.assets ?? [];
65
+ if (!Array.isArray(assets) || assets.length > limits.assets)
66
+ invalid('it has too many assets');
67
+ let size = 0;
68
+ const modules = code.modules.map((module) => {
69
+ if (!isModuleName(module?.name))
70
+ invalid('a module name is not a safe relative path');
71
+ if (!KINDS.includes(module.kind))
72
+ invalid(`module ${module.name} has an unknown kind`);
73
+ size += bounded(module.bytes, limits.moduleSize, `module ${module.name}`);
74
+ return Object.freeze({ name: module.name, kind: module.kind, bytes: module.bytes });
75
+ });
76
+ unique(modules.map((module) => module.name), 'module names');
77
+ if (!modules.some((module) => module.name === code.mainModule)) {
78
+ invalid('its main module is not one of its modules');
79
+ }
80
+ const files = assets.map((asset) => {
81
+ if (!isAssetPath(asset?.path))
82
+ invalid('an asset path is not a canonical absolute URL path');
83
+ if (asset.contentType !== undefined && typeof asset.contentType !== 'string') {
84
+ invalid(`asset ${asset.path} has an invalid content type`);
85
+ }
86
+ size += bounded(asset.bytes, limits.assetSize, `asset ${asset.path}`);
87
+ return Object.freeze({
88
+ path: asset.path,
89
+ bytes: asset.bytes,
90
+ ...(asset.contentType === undefined ? {} : { contentType: asset.contentType }),
91
+ });
92
+ });
93
+ unique(files.map((asset) => asset.path), 'asset paths');
94
+ if (size > limits.workerSize)
95
+ invalid('its files exceed the size of one Worker');
96
+ return Object.freeze({
97
+ manifest,
98
+ mainModule: code.mainModule,
99
+ modules: Object.freeze(modules),
100
+ ...(files.length === 0 ? {} : { assets: Object.freeze(files) }),
101
+ });
102
+ }
103
+ /** The kind a module's name implies: `wasm` for a `.wasm` module, `esm` otherwise. */
104
+ function impliedKind(name) {
105
+ return name.endsWith('.wasm') ? 'wasm' : 'esm';
106
+ }
107
+ function acceptManifest(input) {
108
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
109
+ invalid('its manifest is not an object');
110
+ }
111
+ const manifest = input;
112
+ if (typeof manifest.format !== 'string' || manifest.format.length === 0) {
113
+ invalid('its manifest names no format');
114
+ }
115
+ if (!Number.isSafeInteger(manifest.version) || manifest.version < 1) {
116
+ invalid('its manifest names no version');
117
+ }
118
+ if (Object.hasOwn(manifest, 'entrypoint') || Object.hasOwn(manifest, 'files')) {
119
+ invalid('its manifest declares the entrypoint or the files its host computes');
120
+ }
121
+ let text;
122
+ try {
123
+ text = json.serialize(manifest);
124
+ }
125
+ catch (cause) {
126
+ throw new TypeError('Worker code is invalid: its manifest is not portable JSON.', { cause });
127
+ }
128
+ if (new TextEncoder().encode(text).byteLength > WORKER_DEPLOYMENT_LIMITS.manifestSize) {
129
+ invalid('its manifest is too large');
130
+ }
131
+ return JSON.parse(text);
132
+ }
133
+ function identity(bytes) {
134
+ const { digest, size } = identify(bytes, FILE_MEDIA_TYPE);
135
+ return { digest, size };
136
+ }
137
+ function bounded(bytes, maximum, label) {
138
+ if (!(bytes instanceof Uint8Array))
139
+ invalid(`${label} has no bytes`);
140
+ if (bytes.byteLength > maximum)
141
+ invalid(`${label} exceeds its size limit`);
142
+ return bytes.byteLength;
143
+ }
144
+ /** A safe relative module path: no leading `/`, no `\`, `?` or `#`, no empty, `.` or `..` part. */
145
+ function isModuleName(value) {
146
+ if (typeof value !== 'string' || value.length === 0)
147
+ return false;
148
+ if (value.length > MAXIMUM_MODULE_NAME_LENGTH || /^\/|[\\?#]/u.test(value))
149
+ return false;
150
+ return value.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..');
151
+ }
152
+ /** An absolute canonical URL path: `/`, then non-empty parts other than `.` and `..`. */
153
+ function isAssetPath(value) {
154
+ if (typeof value !== 'string' || value.length > MAXIMUM_ASSET_PATH_LENGTH)
155
+ return false;
156
+ if (!value.startsWith('/') || /[\\?#]/u.test(value))
157
+ return false;
158
+ return value
159
+ .slice(1)
160
+ .split('/')
161
+ .every((part) => part.length > 0 && part !== '.' && part !== '..');
162
+ }
163
+ function unique(values, label) {
164
+ if (new Set(values).size !== values.length)
165
+ invalid(`its ${label} are not unique`);
166
+ }
167
+ function compare(left, right) {
168
+ return left < right ? -1 : left > right ? 1 : 0;
169
+ }
170
+ function invalid(reason) {
171
+ throw new TypeError(`Worker code is invalid: ${reason}.`);
172
+ }
@@ -0,0 +1,73 @@
1
+ import type { Digest } from '@astrale-os/kernel-protocol/artifact';
2
+ export declare const WORKER_CONFIGURATION_FORMAT = "astrale.worker.configuration";
3
+ export declare const WORKER_CONFIGURATION_VERSION = 1;
4
+ /** A Cloudflare Worker binding name. */
5
+ export declare const WORKER_BINDING_NAME: RegExp;
6
+ /**
7
+ * The bindings a Worker host binds itself on every deployment, whatever it asks: never the name
8
+ * of a variable or a secret.
9
+ *
10
+ * - `WORKER_URL`: the deployment URL, `https://<label>.<routing domain>`;
11
+ * - `ASTRALE_BUILD_DIGEST`: the build digest of the uploaded Worker (`workerBuildDigest`);
12
+ * - `ASTRALE_VERSION_METADATA`: the provider version metadata; a Worker that asks for readiness
13
+ * answers its `id` as `x-astrale-worker-version`;
14
+ * - `ASTRALE_SIGNING_IDENTITY`: the private Ed25519 JWK of the deployment, with `signingIdentity`;
15
+ * - `ROUTER`: the host's router service, with `router`;
16
+ * - `ASSETS`: the deployment's static assets, when it uploads some.
17
+ */
18
+ export declare const WORKER_HOST_BINDINGS: readonly string[];
19
+ /**
20
+ * What one Worker deployment freezes beside its code: the Workers runtime it runs on, its plain
21
+ * variables, the names of its secrets (never their values) and what it asks of its host. Its
22
+ * digest and the build digest make the deployment's label, so a changed variable, flag, secret
23
+ * name or option is another deployment, and a rotated secret value is not.
24
+ *
25
+ * - `record`: the deployment serves the record its deployer publishes at
26
+ * `/.well-known/astrale/deployment.json` (the record's bytes are not part of the configuration:
27
+ * a deployment keeps the record it was created with);
28
+ * - `signingIdentity`: the host generates an Ed25519 key for the deployment alone and binds it as
29
+ * `ASTRALE_SIGNING_IDENTITY`;
30
+ * - `router`: the host binds its own router service as `ROUTER`. A deployment never names a
31
+ * service or a dispatch namespace itself.
32
+ *
33
+ * How its host probes readiness is not part of it: a probe changes nothing the deployment serves.
34
+ */
35
+ export interface WorkerConfigurationV1 {
36
+ readonly format: typeof WORKER_CONFIGURATION_FORMAT;
37
+ readonly version: typeof WORKER_CONFIGURATION_VERSION;
38
+ readonly compatibilityDate: string;
39
+ /** Sorted, unique. */
40
+ readonly compatibilityFlags: readonly string[];
41
+ /** Plain variables, never secrets. */
42
+ readonly vars: Readonly<Record<string, string>>;
43
+ /** The names of the secrets the deployment binds, sorted and unique. */
44
+ readonly secrets: readonly string[];
45
+ readonly record: boolean;
46
+ readonly signingIdentity: boolean;
47
+ readonly router: boolean;
48
+ }
49
+ /** A configuration before its canonical order: lists in any order, options false when absent. */
50
+ export interface WorkerConfigurationInput {
51
+ readonly compatibilityDate: string;
52
+ readonly compatibilityFlags?: readonly string[];
53
+ readonly vars?: Readonly<Record<string, string>>;
54
+ readonly secrets?: readonly string[];
55
+ readonly record?: boolean;
56
+ readonly signingIdentity?: boolean;
57
+ readonly router?: boolean;
58
+ }
59
+ /** Freeze one Worker configuration in the one canonical order its digest is taken in. */
60
+ export declare function workerConfiguration(input: WorkerConfigurationInput): WorkerConfigurationV1;
61
+ /**
62
+ * Admit one exact, canonical Worker configuration: every member, lists sorted and unique, binding
63
+ * names Cloudflare admits, distinct across variables and secrets, and none a host binds itself.
64
+ * Anything else throws.
65
+ */
66
+ export declare function acceptWorkerConfiguration(input: unknown): WorkerConfigurationV1;
67
+ /**
68
+ * `sha256` of the canonical JSON (RFC 8785) of one admitted Worker configuration. A deployment's
69
+ * label keeps 80 bits of it with the build digest.
70
+ */
71
+ export declare function workerConfigurationDigest(configuration: WorkerConfigurationV1): Digest;
72
+ /** Whether a configuration is a Worker configuration, by its format alone (not admitted). */
73
+ export declare function isWorkerConfiguration(input: unknown): input is WorkerConfigurationV1;