@astrale-os/sdk 0.6.0-beta.22 → 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.
- package/dist/deployment/adapter/adapter.d.ts +8 -4
- package/dist/deployment/adapter/release/context.d.ts +3 -4
- package/dist/deployment/address/configuration.d.ts +12 -0
- package/dist/deployment/address/configuration.js +11 -0
- package/dist/deployment/address/index.d.ts +2 -2
- package/dist/deployment/address/index.js +2 -2
- package/dist/deployment/address/label.d.ts +5 -5
- package/dist/deployment/address/label.js +5 -5
- package/dist/deployment/address/record.d.ts +21 -0
- package/dist/deployment/address/record.js +34 -0
- package/dist/deployment/verify/index.d.ts +1 -1
- package/dist/deployment/verify/index.js +1 -1
- package/dist/deployment/verify/readiness/index.d.ts +7 -0
- package/dist/deployment/verify/readiness/index.js +17 -0
- package/dist/deployment/worker/address.d.ts +26 -0
- package/dist/deployment/worker/address.js +43 -0
- package/dist/deployment/worker/build.d.ts +60 -0
- package/dist/deployment/worker/build.js +172 -0
- package/dist/deployment/worker/configuration.d.ts +73 -0
- package/dist/deployment/worker/configuration.js +165 -0
- package/dist/deployment/worker/content.d.ts +38 -0
- package/dist/deployment/worker/content.js +169 -0
- package/dist/deployment/worker/index.d.ts +29 -0
- package/dist/deployment/worker/index.js +29 -0
- package/dist/deployment/worker/limits.d.ts +30 -0
- package/dist/deployment/worker/limits.js +30 -0
- package/dist/deployment/worker/record.d.ts +29 -0
- package/dist/deployment/worker/record.js +69 -0
- package/dist/deployment/worker/request.d.ts +118 -0
- package/dist/deployment/worker/request.js +280 -0
- package/dist/deployment/worker/response.d.ts +104 -0
- package/dist/deployment/worker/response.js +213 -0
- package/dist/tooling/cli/immutable-deployment.d.ts +2 -2
- package/package.json +5 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { parseDeploymentLabel } from '../address/label.js';
|
|
2
|
+
import { serviceName } from './address.js';
|
|
3
|
+
import { decodeWorkerRecord } from './record.js';
|
|
4
|
+
export const WORKER_DEPLOYMENT_REJECTIONS = Object.freeze([
|
|
5
|
+
'invalid-deployment',
|
|
6
|
+
'address-mismatch',
|
|
7
|
+
'build-mismatch',
|
|
8
|
+
'service-owned-elsewhere',
|
|
9
|
+
'deployment-conflict',
|
|
10
|
+
'deployment-retired',
|
|
11
|
+
'hosting-unavailable',
|
|
12
|
+
'service-unavailable',
|
|
13
|
+
]);
|
|
14
|
+
const STATES = Object.freeze([
|
|
15
|
+
'deploying',
|
|
16
|
+
'active',
|
|
17
|
+
'retiring',
|
|
18
|
+
'retired',
|
|
19
|
+
'failed',
|
|
20
|
+
]);
|
|
21
|
+
/** Which secret states each deployment state reports. */
|
|
22
|
+
const SECRETS_BY_STATE = Object.freeze({
|
|
23
|
+
created: Object.freeze(['bound']),
|
|
24
|
+
reused: Object.freeze(['updated', 'unchanged']),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Admit the answer of a host's `hosting`: its routing domain. The `addressing: 'opaque'` an older
|
|
28
|
+
* Services answers beside it is admitted and dropped: every Service name is opaque.
|
|
29
|
+
*/
|
|
30
|
+
export function decodeWorkerHosting(input) {
|
|
31
|
+
const value = exact(input, ['routingDomain'], ['addressing'], 'hosting');
|
|
32
|
+
if (typeof value.routingDomain !== 'string' || !isDnsName(value.routingDomain)) {
|
|
33
|
+
invalid('hosting', 'routingDomain');
|
|
34
|
+
}
|
|
35
|
+
if (value.addressing !== undefined && value.addressing !== 'opaque') {
|
|
36
|
+
invalid('hosting', 'addressing');
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({ routingDomain: value.routingDomain });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Admit the answer of a deploy exactly: a deployment whose state reports the secret state it
|
|
42
|
+
* pairs with, or a refusal a host can give.
|
|
43
|
+
*/
|
|
44
|
+
export function decodeWorkerDeploymentResponse(input) {
|
|
45
|
+
const value = exact(input, ['kind'], ['state', 'secrets', 'deployment', 'reason', 'message', 'retryAfter'], 'deploy');
|
|
46
|
+
if (value.kind === 'rejected') {
|
|
47
|
+
exact(value, ['kind', 'reason', 'message'], ['retryAfter'], 'refusal');
|
|
48
|
+
if (!WORKER_DEPLOYMENT_REJECTIONS.includes(value.reason)) {
|
|
49
|
+
invalid('refusal', 'reason');
|
|
50
|
+
}
|
|
51
|
+
if (typeof value.message !== 'string' ||
|
|
52
|
+
value.message.length < 1 ||
|
|
53
|
+
value.message.length > 500) {
|
|
54
|
+
invalid('refusal', 'message');
|
|
55
|
+
}
|
|
56
|
+
if (value.retryAfter !== undefined && !isInstant(value.retryAfter)) {
|
|
57
|
+
invalid('refusal', 'retryAfter');
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
kind: 'rejected',
|
|
61
|
+
reason: value.reason,
|
|
62
|
+
message: value.message,
|
|
63
|
+
...(value.retryAfter === undefined ? {} : { retryAfter: value.retryAfter }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
exact(value, ['kind', 'state', 'secrets', 'deployment'], [], 'deploy');
|
|
67
|
+
if (value.kind !== 'deployed')
|
|
68
|
+
invalid('deploy', 'kind');
|
|
69
|
+
if (value.state !== 'created' && value.state !== 'reused')
|
|
70
|
+
invalid('deploy', 'state');
|
|
71
|
+
const secrets = exact(value.secrets, ['state'], [], 'secrets');
|
|
72
|
+
const allowed = SECRETS_BY_STATE[value.state];
|
|
73
|
+
if (typeof secrets.state !== 'string' || !allowed.includes(secrets.state)) {
|
|
74
|
+
invalid('deploy', 'secrets.state');
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
kind: 'deployed',
|
|
78
|
+
state: value.state,
|
|
79
|
+
secrets: Object.freeze({ state: secrets.state }),
|
|
80
|
+
deployment: decodeWorkerDeploymentSummary(value.deployment),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Admit the answer of a host's `list`: the summaries of the caller's deployments, in its order. */
|
|
84
|
+
export function decodeWorkerDeploymentList(input) {
|
|
85
|
+
const value = exact(input, ['deployments'], [], 'list');
|
|
86
|
+
if (!Array.isArray(value.deployments))
|
|
87
|
+
invalid('list', 'deployments');
|
|
88
|
+
return Object.freeze(value.deployments.map((deployment) => decodeWorkerDeploymentSummary(deployment)));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Admit one summary exactly: node ids and names a person can read, a label whose first part is
|
|
92
|
+
* the Service's name, the URL of that label, a known state, instants, digests, and a record that
|
|
93
|
+
* decodes.
|
|
94
|
+
*/
|
|
95
|
+
export function decodeWorkerDeploymentSummary(input) {
|
|
96
|
+
const value = exact(input, [
|
|
97
|
+
'id',
|
|
98
|
+
'url',
|
|
99
|
+
'label',
|
|
100
|
+
'serviceId',
|
|
101
|
+
'service',
|
|
102
|
+
'project',
|
|
103
|
+
'environment',
|
|
104
|
+
'owner',
|
|
105
|
+
'state',
|
|
106
|
+
'current',
|
|
107
|
+
'createdAt',
|
|
108
|
+
'buildDigest',
|
|
109
|
+
'configuration',
|
|
110
|
+
], ['retiredAt', 'signingKeyId', 'record'], 'deployment');
|
|
111
|
+
for (const key of [
|
|
112
|
+
'id',
|
|
113
|
+
'url',
|
|
114
|
+
'label',
|
|
115
|
+
'serviceId',
|
|
116
|
+
'service',
|
|
117
|
+
'project',
|
|
118
|
+
'environment',
|
|
119
|
+
'owner',
|
|
120
|
+
]) {
|
|
121
|
+
if (typeof value[key] !== 'string' || !isPrintable(value[key]))
|
|
122
|
+
invalid('deployment', key);
|
|
123
|
+
}
|
|
124
|
+
if (value.signingKeyId !== undefined) {
|
|
125
|
+
if (typeof value.signingKeyId !== 'string' || !isPrintable(value.signingKeyId)) {
|
|
126
|
+
invalid('deployment', 'signingKeyId');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let line;
|
|
130
|
+
try {
|
|
131
|
+
line = parseDeploymentLabel(value.label).line;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
invalid('deployment', 'label');
|
|
135
|
+
}
|
|
136
|
+
if (value.service !== line)
|
|
137
|
+
invalid('deployment', 'service');
|
|
138
|
+
let named;
|
|
139
|
+
try {
|
|
140
|
+
named = serviceName(value.project, value.environment);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
invalid('deployment', 'project');
|
|
144
|
+
}
|
|
145
|
+
if (named !== line)
|
|
146
|
+
invalid('deployment', 'service');
|
|
147
|
+
if (!isDeploymentUrl(value.url, value.label))
|
|
148
|
+
invalid('deployment', 'url');
|
|
149
|
+
if (!STATES.includes(value.state))
|
|
150
|
+
invalid('deployment', 'state');
|
|
151
|
+
if (typeof value.current !== 'boolean')
|
|
152
|
+
invalid('deployment', 'current');
|
|
153
|
+
if (!isInstant(value.createdAt))
|
|
154
|
+
invalid('deployment', 'createdAt');
|
|
155
|
+
if (value.retiredAt !== undefined && !isInstant(value.retiredAt))
|
|
156
|
+
invalid('deployment', 'retiredAt');
|
|
157
|
+
for (const key of ['buildDigest', 'configuration']) {
|
|
158
|
+
if (typeof value[key] !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(value[key])) {
|
|
159
|
+
invalid('deployment', key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let record;
|
|
163
|
+
if (value.record !== undefined) {
|
|
164
|
+
try {
|
|
165
|
+
record = decodeWorkerRecord(value.record);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
invalid('deployment', 'record');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const summary = { ...value };
|
|
172
|
+
delete summary.record;
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
...summary,
|
|
175
|
+
...(record === undefined ? {} : { record }),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** `https://<label>.<routing domain>`, nothing more. */
|
|
179
|
+
function isDeploymentUrl(url, label) {
|
|
180
|
+
const prefix = `https://${label}.`;
|
|
181
|
+
return url.startsWith(prefix) && isDnsName(url.slice(prefix.length));
|
|
182
|
+
}
|
|
183
|
+
/** Text a person can read in a listing: at most 512 characters, none of them a control character. */
|
|
184
|
+
function isPrintable(value) {
|
|
185
|
+
return value.length > 0 && value.length <= 512 && !/[\p{Cc}]/u.test(value);
|
|
186
|
+
}
|
|
187
|
+
function isInstant(value) {
|
|
188
|
+
return (typeof value === 'string' &&
|
|
189
|
+
value.length > 0 &&
|
|
190
|
+
value.length <= 64 &&
|
|
191
|
+
!Number.isNaN(Date.parse(value)));
|
|
192
|
+
}
|
|
193
|
+
function isDnsName(value) {
|
|
194
|
+
return (value.length <= 253 &&
|
|
195
|
+
value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label)));
|
|
196
|
+
}
|
|
197
|
+
function exact(input, required, optional, label) {
|
|
198
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
199
|
+
invalid(label);
|
|
200
|
+
const value = input;
|
|
201
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
202
|
+
if (typeof key !== 'string' || (!required.includes(key) && !optional.includes(key))) {
|
|
203
|
+
invalid(label, String(key));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const key of required)
|
|
207
|
+
if (!Object.hasOwn(value, key))
|
|
208
|
+
invalid(label, key);
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function invalid(label, member) {
|
|
212
|
+
throw new TypeError(`The Worker host returned an invalid ${label} answer${member === undefined ? '' : ` (${member})`}.`);
|
|
213
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeploymentCommitV1,
|
|
1
|
+
import type { DeploymentCommitV1, DeploymentConfiguration } from '../../deployment/address/index.js';
|
|
2
2
|
import type { Adapter, Build, Bundle, LegacyAdapter, Placement, Release, ResolvedRuntimeReference } from '../../deployment/index.js';
|
|
3
3
|
import type { DeployResultV1 } from './deploy-result.js';
|
|
4
4
|
/**
|
|
@@ -15,7 +15,7 @@ export interface ImmutableDeploymentPlan {
|
|
|
15
15
|
readonly identity?: string;
|
|
16
16
|
readonly bundle: Bundle;
|
|
17
17
|
readonly secrets: Readonly<Record<string, string>>;
|
|
18
|
-
readonly configuration:
|
|
18
|
+
readonly configuration: DeploymentConfiguration;
|
|
19
19
|
readonly placement: Placement;
|
|
20
20
|
/** `<line>-<content>`: the deployment's DNS label and provider script. */
|
|
21
21
|
readonly label: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/sdk",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.23",
|
|
4
4
|
"description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -167,6 +167,10 @@
|
|
|
167
167
|
"types": "./dist/deployment/address/vectors.d.ts",
|
|
168
168
|
"import": "./dist/deployment/address/vectors.js"
|
|
169
169
|
},
|
|
170
|
+
"./deployment/worker": {
|
|
171
|
+
"types": "./dist/deployment/worker/index.d.ts",
|
|
172
|
+
"import": "./dist/deployment/worker/index.js"
|
|
173
|
+
},
|
|
170
174
|
"./deployment/release": {
|
|
171
175
|
"types": "./dist/deployment/release/index.d.ts",
|
|
172
176
|
"import": "./dist/deployment/release/index.js"
|