@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
@@ -0,0 +1,165 @@
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
+ export const WORKER_CONFIGURATION_FORMAT = 'astrale.worker.configuration';
5
+ export const WORKER_CONFIGURATION_VERSION = 1;
6
+ const MEDIA_TYPE = 'application/vnd.astrale.worker-configuration+json;v=1';
7
+ const COMPATIBILITY_DATE = /^\d{4}-\d{2}-\d{2}$/u;
8
+ const COMPATIBILITY_FLAG = /^[a-z0-9][a-z0-9_]{0,127}$/u;
9
+ /** A Cloudflare Worker binding name. */
10
+ export const WORKER_BINDING_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/u;
11
+ /**
12
+ * The bindings a Worker host binds itself on every deployment, whatever it asks: never the name
13
+ * of a variable or a secret.
14
+ *
15
+ * - `WORKER_URL`: the deployment URL, `https://<label>.<routing domain>`;
16
+ * - `ASTRALE_BUILD_DIGEST`: the build digest of the uploaded Worker (`workerBuildDigest`);
17
+ * - `ASTRALE_VERSION_METADATA`: the provider version metadata; a Worker that asks for readiness
18
+ * answers its `id` as `x-astrale-worker-version`;
19
+ * - `ASTRALE_SIGNING_IDENTITY`: the private Ed25519 JWK of the deployment, with `signingIdentity`;
20
+ * - `ROUTER`: the host's router service, with `router`;
21
+ * - `ASSETS`: the deployment's static assets, when it uploads some.
22
+ */
23
+ export const WORKER_HOST_BINDINGS = Object.freeze([
24
+ 'WORKER_URL',
25
+ 'ASTRALE_BUILD_DIGEST',
26
+ 'ASTRALE_VERSION_METADATA',
27
+ 'ASTRALE_SIGNING_IDENTITY',
28
+ 'ROUTER',
29
+ 'ASSETS',
30
+ ]);
31
+ /** Freeze one Worker configuration in the one canonical order its digest is taken in. */
32
+ export function workerConfiguration(input) {
33
+ if (input === null || typeof input !== 'object')
34
+ invalid('it is not an object');
35
+ return acceptWorkerConfiguration({
36
+ format: WORKER_CONFIGURATION_FORMAT,
37
+ version: WORKER_CONFIGURATION_VERSION,
38
+ compatibilityDate: input.compatibilityDate,
39
+ compatibilityFlags: [...(input.compatibilityFlags ?? [])].sort(compare),
40
+ vars: sortedRecord(input.vars ?? {}),
41
+ secrets: [...(input.secrets ?? [])].sort(compare),
42
+ record: input.record ?? false,
43
+ signingIdentity: input.signingIdentity ?? false,
44
+ router: input.router ?? false,
45
+ });
46
+ }
47
+ /**
48
+ * Admit one exact, canonical Worker configuration: every member, lists sorted and unique, binding
49
+ * names Cloudflare admits, distinct across variables and secrets, and none a host binds itself.
50
+ * Anything else throws.
51
+ */
52
+ export function acceptWorkerConfiguration(input) {
53
+ const value = exact(input, [
54
+ 'format',
55
+ 'version',
56
+ 'compatibilityDate',
57
+ 'compatibilityFlags',
58
+ 'vars',
59
+ 'secrets',
60
+ 'record',
61
+ 'signingIdentity',
62
+ 'router',
63
+ ]);
64
+ if (value.format !== WORKER_CONFIGURATION_FORMAT ||
65
+ value.version !== WORKER_CONFIGURATION_VERSION) {
66
+ invalid('its format is not astrale.worker.configuration v1');
67
+ }
68
+ if (!isDate(value.compatibilityDate))
69
+ invalid('its compatibility date is not YYYY-MM-DD');
70
+ const flags = sortedNames(value.compatibilityFlags, 'compatibility flags', COMPATIBILITY_FLAG, WORKER_DEPLOYMENT_LIMITS.compatibilityFlags);
71
+ const vars = variables(value.vars);
72
+ const secrets = sortedNames(value.secrets, 'secret names', WORKER_BINDING_NAME, WORKER_DEPLOYMENT_LIMITS.secrets);
73
+ const names = [...Object.keys(vars), ...secrets];
74
+ if (new Set(names).size !== names.length)
75
+ invalid('a variable and a secret share a name');
76
+ const reserved = names.filter((name) => WORKER_HOST_BINDINGS.includes(name));
77
+ if (reserved.length > 0)
78
+ invalid(`${reserved.join(', ')} is bound by the host`);
79
+ for (const option of ['record', 'signingIdentity', 'router']) {
80
+ if (typeof value[option] !== 'boolean')
81
+ invalid(`its ${option} option is not a boolean`);
82
+ }
83
+ return Object.freeze({
84
+ format: WORKER_CONFIGURATION_FORMAT,
85
+ version: WORKER_CONFIGURATION_VERSION,
86
+ compatibilityDate: value.compatibilityDate,
87
+ compatibilityFlags: flags,
88
+ vars,
89
+ secrets,
90
+ record: value.record,
91
+ signingIdentity: value.signingIdentity,
92
+ router: value.router,
93
+ });
94
+ }
95
+ /**
96
+ * `sha256` of the canonical JSON (RFC 8785) of one admitted Worker configuration. A deployment's
97
+ * label keeps 80 bits of it with the build digest.
98
+ */
99
+ export function workerConfigurationDigest(configuration) {
100
+ return identify(json.encode(acceptWorkerConfiguration(configuration)), MEDIA_TYPE).digest;
101
+ }
102
+ /** Whether a configuration is a Worker configuration, by its format alone (not admitted). */
103
+ export function isWorkerConfiguration(input) {
104
+ return (input !== null &&
105
+ typeof input === 'object' &&
106
+ input.format === WORKER_CONFIGURATION_FORMAT);
107
+ }
108
+ function variables(input) {
109
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
110
+ invalid('its variables are not an object');
111
+ }
112
+ const keys = Reflect.ownKeys(input);
113
+ if (keys.length > WORKER_DEPLOYMENT_LIMITS.vars)
114
+ invalid('it has too many variables');
115
+ const entries = [];
116
+ for (const name of keys) {
117
+ const text = input[name];
118
+ if (typeof name !== 'string' || !WORKER_BINDING_NAME.test(name)) {
119
+ invalid('a variable name is not a Worker binding name');
120
+ }
121
+ if (typeof text !== 'string' || text.length > WORKER_DEPLOYMENT_LIMITS.valueLength) {
122
+ invalid(`variable ${name} is not text within its size limit`);
123
+ }
124
+ entries.push([name, text]);
125
+ }
126
+ return sortedRecord(Object.fromEntries(entries));
127
+ }
128
+ function sortedRecord(input) {
129
+ return Object.freeze(Object.fromEntries(Object.entries(input).sort(([left], [right]) => compare(left, right))));
130
+ }
131
+ function sortedNames(input, label, pattern, maximum) {
132
+ if (!Array.isArray(input) || input.length > maximum)
133
+ invalid(`its ${label} are invalid`);
134
+ for (const [index, name] of input.entries()) {
135
+ if (typeof name !== 'string' || !pattern.test(name))
136
+ invalid(`its ${label} are invalid`);
137
+ if (index > 0 && compare(input[index - 1], name) >= 0) {
138
+ invalid(`its ${label} are not sorted and unique`);
139
+ }
140
+ }
141
+ return Object.freeze([...input]);
142
+ }
143
+ /** A calendar date written `YYYY-MM-DD`, as a Worker compatibility date is. */
144
+ function isDate(input) {
145
+ if (typeof input !== 'string' || !COMPATIBILITY_DATE.test(input))
146
+ return false;
147
+ const time = Date.parse(`${input}T00:00:00Z`);
148
+ return !Number.isNaN(time) && new Date(time).toISOString().slice(0, 10) === input;
149
+ }
150
+ function exact(input, keys) {
151
+ if (input === null ||
152
+ typeof input !== 'object' ||
153
+ Array.isArray(input) ||
154
+ Reflect.ownKeys(input).length !== keys.length ||
155
+ keys.some((key) => !Object.hasOwn(input, key))) {
156
+ invalid(`its members are not exactly ${keys.join(', ')}`);
157
+ }
158
+ return input;
159
+ }
160
+ function compare(left, right) {
161
+ return left < right ? -1 : left > right ? 1 : 0;
162
+ }
163
+ function invalid(reason) {
164
+ throw new TypeError(`Worker configuration is invalid: ${reason}.`);
165
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * One file of a Worker deployment on the wire: its bytes as portable Base64, gzip-compressed when
3
+ * that is smaller, and their decoded size. The encoding is transport only: every digest and every
4
+ * upload describes the decoded bytes. Base64 longer than one portable chunk travels in chunks.
5
+ */
6
+ export type WorkerContentV1 = {
7
+ readonly encoding: 'identity' | 'gzip';
8
+ readonly decodedSize: number;
9
+ } & ({
10
+ readonly contentBase64: string;
11
+ } | {
12
+ readonly contentBase64Chunks: readonly string[];
13
+ });
14
+ /**
15
+ * Encode one file for the wire: gzip when that is smaller, identity otherwise. Compression runs
16
+ * on `CompressionStream`, in Node and in a Worker alike.
17
+ */
18
+ export declare function encodeWorkerContent(bytes: Uint8Array): Promise<WorkerContentV1>;
19
+ /**
20
+ * Decode one file from the wire, refusing anything that is not exactly this envelope: canonical
21
+ * Base64 within `base64Size` characters (chunks of at most one portable chunk each), a declared
22
+ * size within `limit`, and a gzip stream that inflates to exactly that size. Inflation is bounded
23
+ * by the declared size as it runs, so a high-ratio stream is stopped before it allocates more.
24
+ */
25
+ export declare function decodeWorkerContent(input: unknown, limits: {
26
+ readonly size: number;
27
+ readonly base64Size: number;
28
+ }): Promise<Uint8Array>;
29
+ /** Keep every wire string below Core's portable Value string limit. */
30
+ export declare function portableBase64(bytes: Uint8Array): {
31
+ readonly contentBase64: string;
32
+ } | {
33
+ readonly contentBase64Chunks: readonly string[];
34
+ };
35
+ /** Standard Base64 with padding, without Node's `Buffer`. */
36
+ export declare function toBase64(bytes: Uint8Array): string;
37
+ /** Canonical standard Base64 only: the encoding `toBase64` writes for the same bytes. */
38
+ export declare function fromBase64(encoded: string): Uint8Array<ArrayBuffer>;
@@ -0,0 +1,169 @@
1
+ import { WORKER_DEPLOYMENT_LIMITS } from './limits.js';
2
+ /** Gzip's header and trailer cost twenty bytes: smaller files are never worth compressing. */
3
+ const GZIP_THRESHOLD = 64;
4
+ /** The compressed bytes fed to the inflater at a time, so an overflow is caught early. */
5
+ const INFLATE_CHUNK = 4 * 1024;
6
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/u;
7
+ /**
8
+ * Encode one file for the wire: gzip when that is smaller, identity otherwise. Compression runs
9
+ * on `CompressionStream`, in Node and in a Worker alike.
10
+ */
11
+ export async function encodeWorkerContent(bytes) {
12
+ const compressed = bytes.byteLength < GZIP_THRESHOLD ? undefined : await gzip(bytes);
13
+ const smaller = compressed !== undefined && compressed.byteLength < bytes.byteLength;
14
+ return Object.freeze({
15
+ encoding: smaller ? 'gzip' : 'identity',
16
+ decodedSize: bytes.byteLength,
17
+ ...portableBase64(smaller ? compressed : bytes),
18
+ });
19
+ }
20
+ /**
21
+ * Decode one file from the wire, refusing anything that is not exactly this envelope: canonical
22
+ * Base64 within `base64Size` characters (chunks of at most one portable chunk each), a declared
23
+ * size within `limit`, and a gzip stream that inflates to exactly that size. Inflation is bounded
24
+ * by the declared size as it runs, so a high-ratio stream is stopped before it allocates more.
25
+ */
26
+ export async function decodeWorkerContent(input, limits) {
27
+ const value = record(input);
28
+ const chunked = Object.hasOwn(value, 'contentBase64Chunks');
29
+ exact(value, ['encoding', 'decodedSize', chunked ? 'contentBase64Chunks' : 'contentBase64']);
30
+ const { encoding, decodedSize } = value;
31
+ if (encoding !== 'identity' && encoding !== 'gzip')
32
+ invalid('its encoding is unknown');
33
+ if (!Number.isSafeInteger(decodedSize) || decodedSize < 0) {
34
+ invalid('its decoded size is invalid');
35
+ }
36
+ if (decodedSize > limits.size)
37
+ invalid('it exceeds its size limit');
38
+ const encoded = joined(value, chunked, limits.base64Size);
39
+ const bytes = fromBase64(encoded);
40
+ if (encoding === 'gzip')
41
+ return inflate(bytes, decodedSize);
42
+ if (bytes.byteLength !== decodedSize)
43
+ invalid('it differs from its declared size');
44
+ return bytes;
45
+ }
46
+ /** Keep every wire string below Core's portable Value string limit. */
47
+ export function portableBase64(bytes) {
48
+ const encoded = toBase64(bytes);
49
+ const size = WORKER_DEPLOYMENT_LIMITS.base64ChunkSize;
50
+ if (encoded.length <= size)
51
+ return Object.freeze({ contentBase64: encoded });
52
+ const chunks = [];
53
+ for (let offset = 0; offset < encoded.length; offset += size) {
54
+ chunks.push(encoded.slice(offset, offset + size));
55
+ }
56
+ return Object.freeze({ contentBase64Chunks: Object.freeze(chunks) });
57
+ }
58
+ /** Standard Base64 with padding, without Node's `Buffer`. */
59
+ export function toBase64(bytes) {
60
+ let binary = '';
61
+ for (let offset = 0; offset < bytes.byteLength; offset += 0x8000) {
62
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
63
+ }
64
+ return btoa(binary);
65
+ }
66
+ /** Canonical standard Base64 only: the encoding `toBase64` writes for the same bytes. */
67
+ export function fromBase64(encoded) {
68
+ if (encoded.length % 4 !== 0 || !BASE64.test(encoded))
69
+ invalid('it is not canonical Base64');
70
+ let binary;
71
+ try {
72
+ binary = atob(encoded);
73
+ }
74
+ catch {
75
+ invalid('it is not canonical Base64');
76
+ }
77
+ const bytes = new Uint8Array(binary.length);
78
+ for (let index = 0; index < binary.length; index += 1)
79
+ bytes[index] = binary.charCodeAt(index);
80
+ if (btoa(binary) !== encoded)
81
+ invalid('it is not canonical Base64');
82
+ return bytes;
83
+ }
84
+ function joined(value, chunked, maximum) {
85
+ if (!chunked) {
86
+ const text = value.contentBase64;
87
+ if (typeof text !== 'string' || text.length > maximum)
88
+ invalid('its Base64 is too long');
89
+ return text;
90
+ }
91
+ const chunks = value.contentBase64Chunks;
92
+ const size = WORKER_DEPLOYMENT_LIMITS.base64ChunkSize;
93
+ if (!Array.isArray(chunks) || chunks.length === 0 || chunks.length > Math.ceil(maximum / size)) {
94
+ invalid('its Base64 chunks are invalid');
95
+ }
96
+ let length = 0;
97
+ for (const chunk of chunks) {
98
+ if (typeof chunk !== 'string' || chunk.length === 0 || chunk.length > size) {
99
+ invalid('its Base64 chunks are invalid');
100
+ }
101
+ length += chunk.length;
102
+ }
103
+ if (length > maximum)
104
+ invalid('its Base64 is too long');
105
+ return chunks.join('');
106
+ }
107
+ async function gzip(bytes) {
108
+ const stream = new Blob([bytes])
109
+ .stream()
110
+ .pipeThrough(new CompressionStream('gzip'));
111
+ return new Uint8Array(await new Response(stream).arrayBuffer());
112
+ }
113
+ async function inflate(bytes, decodedSize) {
114
+ // Preallocate only the admitted size and copy as bytes arrive: never collect an unbounded inflate.
115
+ const decoded = new Uint8Array(decodedSize);
116
+ let offset = 0;
117
+ const source = new ReadableStream({
118
+ pull(controller) {
119
+ if (offset === bytes.byteLength) {
120
+ controller.close();
121
+ return;
122
+ }
123
+ const end = Math.min(offset + INFLATE_CHUNK, bytes.byteLength);
124
+ controller.enqueue(bytes.subarray(offset, end));
125
+ offset = end;
126
+ },
127
+ }, { highWaterMark: 0 });
128
+ const reader = source.pipeThrough(new DecompressionStream('gzip')).getReader();
129
+ let written = 0;
130
+ try {
131
+ for (;;) {
132
+ const { value, done } = await reader.read();
133
+ if (done)
134
+ break;
135
+ if (value.byteLength > decodedSize - written)
136
+ invalid('it exceeds its declared size');
137
+ decoded.set(value, written);
138
+ written += value.byteLength;
139
+ }
140
+ }
141
+ catch (cause) {
142
+ await reader.cancel(cause).catch(() => undefined);
143
+ if (cause instanceof TypeError && cause.message.startsWith(PREFIX))
144
+ throw cause;
145
+ throw new TypeError(`${PREFIX} its gzip stream is invalid.`, { cause });
146
+ }
147
+ finally {
148
+ reader.releaseLock();
149
+ }
150
+ if (written !== decodedSize)
151
+ invalid('it differs from its declared size');
152
+ return decoded;
153
+ }
154
+ const PREFIX = 'Worker deployment content is invalid:';
155
+ function record(input) {
156
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
157
+ invalid('it is not an object');
158
+ }
159
+ return input;
160
+ }
161
+ function exact(value, keys) {
162
+ const own = Reflect.ownKeys(value);
163
+ if (own.length !== keys.length || keys.some((key) => !Object.hasOwn(value, key))) {
164
+ invalid('its members are not exactly encoding, decodedSize and its Base64');
165
+ }
166
+ }
167
+ function invalid(reason) {
168
+ throw new TypeError(`${PREFIX} ${reason}.`);
169
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@astrale-os/sdk/deployment/worker` — the contract of a Worker host (Admin's Services), agnostic
3
+ * of what the Worker serves: an Astrale Domain release, or any Cloudflare Worker (a cron job, an
4
+ * API). A deployment is one Worker release in the Service of a project and an environment:
5
+ *
6
+ * - its code (`WorkerCode`): modules, a main module, static assets, and the build manifest members
7
+ * its deployer declares; `workerBuildDigest` identifies it, and its host binds that digest as
8
+ * `ASTRALE_BUILD_DIGEST`;
9
+ * - its configuration (`WorkerConfigurationV1`): the Workers runtime, plain variables, the names of
10
+ * its secrets, and what it asks of its host (`record`, `signingIdentity`, `router`);
11
+ * - its label, `<service>-<content>` (`workerDeploymentAddress`): the Service name derives from the
12
+ * project and environment, the content from the two digests, so the same release reuses its
13
+ * deployment;
14
+ * - its record (`WorkerRecord`): opaque bytes and a media type its host serves verbatim at
15
+ * `/.well-known/astrale/deployment.json`;
16
+ * - readiness paths its host probes before activation, and secret values it never stores.
17
+ *
18
+ * `encodeWorkerDeploymentRequest` builds the request a deployer sends, and
19
+ * `decodeWorkerDeploymentRequest` admits it as a host does; the response decoders admit what a
20
+ * host answers.
21
+ */
22
+ export { PROJECT_NAME, serviceName, workerDeploymentLabel } from './address.js';
23
+ export { acceptWorkerCode, WORKER_BUILD_MANIFEST, workerBuildDigest, workerBuildManifest, type WorkerAsset, type WorkerBuildManifest, type WorkerCode, type WorkerModule, type WorkerModuleKind, } from './build.js';
24
+ export { acceptWorkerConfiguration, isWorkerConfiguration, WORKER_BINDING_NAME, WORKER_CONFIGURATION_FORMAT, WORKER_CONFIGURATION_VERSION, WORKER_HOST_BINDINGS, workerConfiguration, workerConfigurationDigest, type WorkerConfigurationInput, type WorkerConfigurationV1, } from './configuration.js';
25
+ export { decodeWorkerContent, encodeWorkerContent, type WorkerContentV1 } from './content.js';
26
+ export { WORKER_DEPLOYMENT_LIMITS } from './limits.js';
27
+ export { acceptWorkerRecord, decodeWorkerRecord, encodeWorkerRecord, sameWorkerRecord, WORKER_RECORD_PATH, type WorkerRecord, type WorkerRecordWireV1, } from './record.js';
28
+ export { decodeWorkerDeploymentRequest, encodeWorkerDeploymentRequest, WORKER_DEPLOYMENT_REQUEST_FORMAT, WORKER_DEPLOYMENT_REQUEST_VERSION, workerDeploymentAddress, WorkerDeploymentRefusal, type WorkerCodeWireV1, type WorkerDeployment, type WorkerDeploymentAddress, type WorkerDeploymentInput, type WorkerDeploymentRequestV1, } from './request.js';
29
+ export { decodeWorkerDeploymentList, decodeWorkerDeploymentResponse, decodeWorkerDeploymentSummary, decodeWorkerHosting, WORKER_DEPLOYMENT_REJECTIONS, type WorkerDeploymentListRequestV1, type WorkerDeploymentRejection, type WorkerDeploymentRejectionReason, type WorkerDeploymentResponseV1, type WorkerDeploymentResult, type WorkerDeploymentState, type WorkerDeploymentSummary, type WorkerDeploymentSummaryV1, type WorkerHostingV1, } from './response.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@astrale-os/sdk/deployment/worker` — the contract of a Worker host (Admin's Services), agnostic
3
+ * of what the Worker serves: an Astrale Domain release, or any Cloudflare Worker (a cron job, an
4
+ * API). A deployment is one Worker release in the Service of a project and an environment:
5
+ *
6
+ * - its code (`WorkerCode`): modules, a main module, static assets, and the build manifest members
7
+ * its deployer declares; `workerBuildDigest` identifies it, and its host binds that digest as
8
+ * `ASTRALE_BUILD_DIGEST`;
9
+ * - its configuration (`WorkerConfigurationV1`): the Workers runtime, plain variables, the names of
10
+ * its secrets, and what it asks of its host (`record`, `signingIdentity`, `router`);
11
+ * - its label, `<service>-<content>` (`workerDeploymentAddress`): the Service name derives from the
12
+ * project and environment, the content from the two digests, so the same release reuses its
13
+ * deployment;
14
+ * - its record (`WorkerRecord`): opaque bytes and a media type its host serves verbatim at
15
+ * `/.well-known/astrale/deployment.json`;
16
+ * - readiness paths its host probes before activation, and secret values it never stores.
17
+ *
18
+ * `encodeWorkerDeploymentRequest` builds the request a deployer sends, and
19
+ * `decodeWorkerDeploymentRequest` admits it as a host does; the response decoders admit what a
20
+ * host answers.
21
+ */
22
+ export { PROJECT_NAME, serviceName, workerDeploymentLabel } from './address.js';
23
+ export { acceptWorkerCode, WORKER_BUILD_MANIFEST, workerBuildDigest, workerBuildManifest, } from './build.js';
24
+ export { acceptWorkerConfiguration, isWorkerConfiguration, WORKER_BINDING_NAME, WORKER_CONFIGURATION_FORMAT, WORKER_CONFIGURATION_VERSION, WORKER_HOST_BINDINGS, workerConfiguration, workerConfigurationDigest, } from './configuration.js';
25
+ export { decodeWorkerContent, encodeWorkerContent } from './content.js';
26
+ export { WORKER_DEPLOYMENT_LIMITS } from './limits.js';
27
+ export { acceptWorkerRecord, decodeWorkerRecord, encodeWorkerRecord, sameWorkerRecord, WORKER_RECORD_PATH, } from './record.js';
28
+ export { decodeWorkerDeploymentRequest, encodeWorkerDeploymentRequest, WORKER_DEPLOYMENT_REQUEST_FORMAT, WORKER_DEPLOYMENT_REQUEST_VERSION, workerDeploymentAddress, WorkerDeploymentRefusal, } from './request.js';
29
+ export { decodeWorkerDeploymentList, decodeWorkerDeploymentResponse, decodeWorkerDeploymentSummary, decodeWorkerHosting, WORKER_DEPLOYMENT_REJECTIONS, } from './response.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The bounds of one Worker deployment on the wire and once decoded. A host refuses a request past
3
+ * any of them before it reads or writes anything; a deployer refuses one before it sends it.
4
+ */
5
+ export declare const WORKER_DEPLOYMENT_LIMITS: Readonly<{
6
+ /** Every wire string stays below Core's portable Value string limit. */
7
+ base64ChunkSize: number;
8
+ /** The Base64 of one module, chunks joined. */
9
+ moduleBase64Size: number;
10
+ /** The Base64 of one asset, chunks joined. */
11
+ assetBase64Size: number;
12
+ /** The decoded bytes of one module. */
13
+ moduleSize: number;
14
+ /** The decoded bytes of one asset. */
15
+ assetSize: number;
16
+ /** The decoded bytes of every module and asset of one Worker together. */
17
+ workerSize: number;
18
+ modules: 40;
19
+ assets: 2000;
20
+ /** The bytes of one deployment record. */
21
+ recordSize: number;
22
+ /** The canonical JSON bytes of the build manifest members a deployer declares. */
23
+ manifestSize: number;
24
+ vars: 128;
25
+ /** The UTF-16 length of one variable or secret value. */
26
+ valueLength: number;
27
+ secrets: 64;
28
+ compatibilityFlags: 64;
29
+ readinessPaths: 8;
30
+ }>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The bounds of one Worker deployment on the wire and once decoded. A host refuses a request past
3
+ * any of them before it reads or writes anything; a deployer refuses one before it sends it.
4
+ */
5
+ export const WORKER_DEPLOYMENT_LIMITS = Object.freeze({
6
+ /** Every wire string stays below Core's portable Value string limit. */
7
+ base64ChunkSize: 512 * 1024,
8
+ /** The Base64 of one module, chunks joined. */
9
+ moduleBase64Size: 10 * 1024 * 1024,
10
+ /** The Base64 of one asset, chunks joined. */
11
+ assetBase64Size: 16 * 1024 * 1024,
12
+ /** The decoded bytes of one module. */
13
+ moduleSize: ((10 * 1024 * 1024) / 4) * 3,
14
+ /** The decoded bytes of one asset. */
15
+ assetSize: ((16 * 1024 * 1024) / 4) * 3,
16
+ /** The decoded bytes of every module and asset of one Worker together. */
17
+ workerSize: 32 * 1024 * 1024,
18
+ modules: 40,
19
+ assets: 2_000,
20
+ /** The bytes of one deployment record. */
21
+ recordSize: 64 * 1024,
22
+ /** The canonical JSON bytes of the build manifest members a deployer declares. */
23
+ manifestSize: 4 * 1024,
24
+ vars: 128,
25
+ /** The UTF-16 length of one variable or secret value. */
26
+ valueLength: 64 * 1024,
27
+ secrets: 64,
28
+ compatibilityFlags: 64,
29
+ readinessPaths: 8,
30
+ });
@@ -0,0 +1,29 @@
1
+ /** Where a Worker host serves a deployment's record, which the deployment itself never answers. */
2
+ export declare const WORKER_RECORD_PATH = "/.well-known/astrale/deployment.json";
3
+ /**
4
+ * The record a deployer publishes for one deployment: opaque bytes and their media type. Its host
5
+ * serves it verbatim at `WORKER_RECORD_PATH`, immutable, and never reads it: what it says is the
6
+ * deployer's (a Domain deployer publishes its `DeploymentRecordV1`). A deployment keeps the record
7
+ * it was created with.
8
+ */
9
+ export interface WorkerRecord {
10
+ /** A media type, served verbatim as the record's `content-type`. */
11
+ readonly contentType: string;
12
+ /** One to `WORKER_DEPLOYMENT_LIMITS.recordSize` bytes. */
13
+ readonly bytes: Uint8Array;
14
+ }
15
+ /** One record on the wire: its media type and its bytes as canonical Base64. */
16
+ export interface WorkerRecordWireV1 {
17
+ readonly contentType: string;
18
+ readonly contentBase64: string;
19
+ }
20
+ /** Admit one record: a media type a response can carry as written, and bounded bytes. */
21
+ export declare function acceptWorkerRecord(input: WorkerRecord): WorkerRecord;
22
+ /** Whether two records are the same publication: the same media type and the same bytes. */
23
+ export declare function sameWorkerRecord(left: WorkerRecord, right: WorkerRecord): boolean;
24
+ /** Whether `input` is a media type a record may carry, exactly as it will be served. */
25
+ export declare function isMediaType(input: unknown): input is string;
26
+ /** One record on the wire. */
27
+ export declare function encodeWorkerRecord(record: WorkerRecord): WorkerRecordWireV1;
28
+ /** Decode one record from the wire: exactly a media type and canonical Base64 of bounded bytes. */
29
+ export declare function decodeWorkerRecord(input: unknown): WorkerRecord;
@@ -0,0 +1,69 @@
1
+ import { fromBase64, toBase64 } from './content.js';
2
+ import { WORKER_DEPLOYMENT_LIMITS } from './limits.js';
3
+ /** Where a Worker host serves a deployment's record, which the deployment itself never answers. */
4
+ export const WORKER_RECORD_PATH = '/.well-known/astrale/deployment.json';
5
+ const TOKEN = "[A-Za-z0-9!#$%&'*+.^_`|~-]+";
6
+ /** `type/subtype` and `;name=value` parameters (token or quoted value), at most 256 characters. */
7
+ const MEDIA_TYPE = new RegExp(`^${TOKEN}/${TOKEN}(?:[ \\t]*;[ \\t]*${TOKEN}=(?:${TOKEN}|"[\\x20\\x21\\x23-\\x5b\\x5d-\\x7e]*"))*$`, 'u');
8
+ const MAXIMUM_MEDIA_TYPE_LENGTH = 256;
9
+ /** Admit one record: a media type a response can carry as written, and bounded bytes. */
10
+ export function acceptWorkerRecord(input) {
11
+ if (input === null || typeof input !== 'object')
12
+ invalid('it is not an object');
13
+ if (!isMediaType(input.contentType))
14
+ invalid('its content type is not a media type');
15
+ if (!(input.bytes instanceof Uint8Array))
16
+ invalid('its bytes are not bytes');
17
+ if (input.bytes.byteLength === 0 ||
18
+ input.bytes.byteLength > WORKER_DEPLOYMENT_LIMITS.recordSize) {
19
+ invalid('its size is outside its limit');
20
+ }
21
+ return Object.freeze({ contentType: input.contentType, bytes: input.bytes });
22
+ }
23
+ /** Whether two records are the same publication: the same media type and the same bytes. */
24
+ export function sameWorkerRecord(left, right) {
25
+ if (left.contentType !== right.contentType)
26
+ return false;
27
+ if (left.bytes.byteLength !== right.bytes.byteLength)
28
+ return false;
29
+ return left.bytes.every((byte, index) => byte === right.bytes[index]);
30
+ }
31
+ /** Whether `input` is a media type a record may carry, exactly as it will be served. */
32
+ export function isMediaType(input) {
33
+ return (typeof input === 'string' && input.length <= MAXIMUM_MEDIA_TYPE_LENGTH && MEDIA_TYPE.test(input));
34
+ }
35
+ /** One record on the wire. */
36
+ export function encodeWorkerRecord(record) {
37
+ const admitted = acceptWorkerRecord(record);
38
+ return Object.freeze({
39
+ contentType: admitted.contentType,
40
+ contentBase64: toBase64(admitted.bytes),
41
+ });
42
+ }
43
+ /** Decode one record from the wire: exactly a media type and canonical Base64 of bounded bytes. */
44
+ export function decodeWorkerRecord(input) {
45
+ if (input === null ||
46
+ typeof input !== 'object' ||
47
+ Array.isArray(input) ||
48
+ Reflect.ownKeys(input).length !== 2 ||
49
+ !Object.hasOwn(input, 'contentType') ||
50
+ !Object.hasOwn(input, 'contentBase64')) {
51
+ invalid('its members are not exactly contentType and contentBase64');
52
+ }
53
+ const { contentType, contentBase64 } = input;
54
+ if (typeof contentBase64 !== 'string' ||
55
+ contentBase64.length > Math.ceil(WORKER_DEPLOYMENT_LIMITS.recordSize / 3) * 4) {
56
+ invalid('its Base64 is invalid');
57
+ }
58
+ let bytes;
59
+ try {
60
+ bytes = fromBase64(contentBase64);
61
+ }
62
+ catch (cause) {
63
+ throw new TypeError('Worker record is invalid: its Base64 is invalid.', { cause });
64
+ }
65
+ return acceptWorkerRecord({ contentType: contentType, bytes });
66
+ }
67
+ function invalid(reason) {
68
+ throw new TypeError(`Worker record is invalid: ${reason}.`);
69
+ }