@hypequery/deployment 0.0.0-canary-20260719200737 → 0.2.0
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/README.md +2 -95
- package/dist/activation.d.ts +0 -11
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +19 -100
- package/dist/index.d.ts +2 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -7
- package/package.json +3 -3
- package/dist/control-plane-adapters.d.ts +0 -7
- package/dist/control-plane-adapters.d.ts.map +0 -1
- package/dist/control-plane-adapters.js +0 -163
- package/dist/control-plane-limits.d.ts +0 -7
- package/dist/control-plane-limits.d.ts.map +0 -1
- package/dist/control-plane-limits.js +0 -18
- package/dist/control-plane.d.ts +0 -35
- package/dist/control-plane.d.ts.map +0 -1
- package/dist/control-plane.js +0 -362
- package/dist/node-runtime-factory.d.ts +0 -16
- package/dist/node-runtime-factory.d.ts.map +0 -1
- package/dist/node-runtime-factory.js +0 -274
- package/dist/runtime-materialization.d.ts +0 -58
- package/dist/runtime-materialization.d.ts.map +0 -1
- package/dist/runtime-materialization.js +0 -187
- package/dist/runtime-supervisor.d.ts +0 -70
- package/dist/runtime-supervisor.d.ts.map +0 -1
- package/dist/runtime-supervisor.js +0 -254
package/dist/control-plane.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
|
|
2
|
-
import { type DeploymentActivationRegistry } from './activation.js';
|
|
3
|
-
import { type DeploymentControlPlaneLimits } from './control-plane-limits.js';
|
|
4
|
-
import type { DeploymentAuthenticator, DeploymentIntake, DeploymentIntakeRequest, DeploymentIntakeResponse } from './types.js';
|
|
5
|
-
export type DeploymentControlPlaneAction = 'activate' | 'read-current-activation' | 'read-activation-history';
|
|
6
|
-
export interface DeploymentControlPlaneAuthorizationInput<Principal> {
|
|
7
|
-
readonly principal: Principal;
|
|
8
|
-
readonly action: DeploymentControlPlaneAction;
|
|
9
|
-
readonly target: ProtocolDeploymentReleaseTarget;
|
|
10
|
-
readonly signal?: AbortSignal;
|
|
11
|
-
}
|
|
12
|
-
export interface DeploymentControlPlaneAuthorizer<Principal> {
|
|
13
|
-
authorize(input: DeploymentControlPlaneAuthorizationInput<Principal>): Promise<boolean>;
|
|
14
|
-
}
|
|
15
|
-
export interface DeploymentControlPlaneRequest extends DeploymentIntakeRequest {
|
|
16
|
-
readonly method: string;
|
|
17
|
-
readonly path: string;
|
|
18
|
-
readonly query?: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
19
|
-
/** Set by transport adapters without consuming the request stream. */
|
|
20
|
-
readonly hasBody?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export type DeploymentControlPlaneResponse = DeploymentIntakeResponse;
|
|
23
|
-
export interface DeploymentControlPlane {
|
|
24
|
-
handle(request: DeploymentControlPlaneRequest): Promise<DeploymentControlPlaneResponse>;
|
|
25
|
-
}
|
|
26
|
-
export interface DeploymentControlPlaneOptions<Principal> {
|
|
27
|
-
readonly intake: DeploymentIntake;
|
|
28
|
-
readonly activations: DeploymentActivationRegistry;
|
|
29
|
-
readonly authenticator: DeploymentAuthenticator<Principal>;
|
|
30
|
-
readonly authorizer: DeploymentControlPlaneAuthorizer<Principal>;
|
|
31
|
-
readonly limits?: Partial<DeploymentControlPlaneLimits>;
|
|
32
|
-
}
|
|
33
|
-
export type DeploymentControlPlaneErrorCode = 'HQ_CONTROL_BAD_REQUEST' | 'HQ_CONTROL_UNAUTHENTICATED' | 'HQ_CONTROL_FORBIDDEN' | 'HQ_CONTROL_NOT_FOUND' | 'HQ_CONTROL_METHOD_NOT_ALLOWED' | 'HQ_CONTROL_TOO_LARGE' | 'HQ_CONTROL_RELEASE_NOT_FOUND' | 'HQ_CONTROL_RELEASE_UNAVAILABLE' | 'HQ_CONTROL_INTERNAL';
|
|
34
|
-
export declare function createDeploymentControlPlane<Principal>(options: DeploymentControlPlaneOptions<Principal>): DeploymentControlPlane;
|
|
35
|
-
//# sourceMappingURL=control-plane.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,4BAA4B,GACpC,UAAU,GACV,yBAAyB,GACzB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,wCAAwC,CAAC,SAAS;IACjE,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,4BAA4B,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,gCAAgC,CAAC,SAAS;IACzD,SAAS,CAAC,KAAK,EAAE,wCAAwC,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzF;AAED,MAAM,WAAW,6BAA8B,SAAQ,uBAAuB;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;IAClF,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,8BAA8B,GAAG,wBAAwB,CAAC;AAEtE,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;CACzF;AAED,MAAM,WAAW,6BAA6B,CAAC,SAAS;IACtD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,4BAA4B,CAAC;IACnD,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC3D,QAAQ,CAAC,UAAU,EAAE,gCAAgC,CAAC,SAAS,CAAC,CAAC;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,4BAA4B,CAAC,CAAC;CACzD;AAED,MAAM,MAAM,+BAA+B,GACvC,wBAAwB,GACxB,4BAA4B,GAC5B,sBAAsB,GACtB,sBAAsB,GACtB,+BAA+B,GAC/B,sBAAsB,GACtB,8BAA8B,GAC9B,gCAAgC,GAChC,qBAAqB,CAAC;AA+X1B,wBAAgB,4BAA4B,CAAC,SAAS,EACpD,OAAO,EAAE,6BAA6B,CAAC,SAAS,CAAC,GAChD,sBAAsB,CAkHxB"}
|
package/dist/control-plane.js
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
|
|
2
|
-
import { DeploymentActivationError, } from './activation.js';
|
|
3
|
-
import { resolveDeploymentControlPlaneLimits, } from './control-plane-limits.js';
|
|
4
|
-
const IDENTITY_PATTERN = /^[0-9a-f]{64}$/;
|
|
5
|
-
const JSON_CONTENT_TYPE = /^application\/json(?:;\s*charset=utf-8)?$/i;
|
|
6
|
-
const textDecoder = new TextDecoder('utf-8', { fatal: true });
|
|
7
|
-
const ERROR_STATUS = Object.freeze({
|
|
8
|
-
HQ_CONTROL_BAD_REQUEST: 400,
|
|
9
|
-
HQ_CONTROL_UNAUTHENTICATED: 401,
|
|
10
|
-
HQ_CONTROL_FORBIDDEN: 403,
|
|
11
|
-
HQ_CONTROL_NOT_FOUND: 404,
|
|
12
|
-
HQ_CONTROL_METHOD_NOT_ALLOWED: 405,
|
|
13
|
-
HQ_CONTROL_TOO_LARGE: 413,
|
|
14
|
-
HQ_CONTROL_RELEASE_NOT_FOUND: 404,
|
|
15
|
-
HQ_CONTROL_RELEASE_UNAVAILABLE: 503,
|
|
16
|
-
HQ_CONTROL_INTERNAL: 500,
|
|
17
|
-
});
|
|
18
|
-
class ControlPlaneError extends Error {
|
|
19
|
-
code;
|
|
20
|
-
status;
|
|
21
|
-
expose;
|
|
22
|
-
headers;
|
|
23
|
-
constructor(code, message, options = {}) {
|
|
24
|
-
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
25
|
-
this.name = 'DeploymentControlPlaneError';
|
|
26
|
-
this.code = code;
|
|
27
|
-
this.status = ERROR_STATUS[code];
|
|
28
|
-
this.expose = options.expose ?? this.status < 500;
|
|
29
|
-
this.headers = Object.freeze({ ...options.headers });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function requestHeader(headers, expectedName) {
|
|
33
|
-
const values = Object.entries(headers)
|
|
34
|
-
.filter(([name, value]) => value !== undefined && name.toLowerCase() === expectedName)
|
|
35
|
-
.map(([, value]) => value);
|
|
36
|
-
if (values.length > 1) {
|
|
37
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', `Duplicate ${expectedName} header.`);
|
|
38
|
-
}
|
|
39
|
-
return values[0];
|
|
40
|
-
}
|
|
41
|
-
function bearerToken(headers) {
|
|
42
|
-
const authorization = requestHeader(headers, 'authorization');
|
|
43
|
-
const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : '';
|
|
44
|
-
if (token.length < 1 || token.length > 4096 || token.trim() !== token
|
|
45
|
-
|| [...token].some(character => {
|
|
46
|
-
const code = character.charCodeAt(0);
|
|
47
|
-
return code < 0x21 || code > 0x7e;
|
|
48
|
-
})) {
|
|
49
|
-
throw new ControlPlaneError('HQ_CONTROL_UNAUTHENTICATED', 'A valid bearer credential is required.', { headers: { 'www-authenticate': 'Bearer' } });
|
|
50
|
-
}
|
|
51
|
-
return token;
|
|
52
|
-
}
|
|
53
|
-
function jsonResponse(status, body, headers = {}) {
|
|
54
|
-
const encoded = `${JSON.stringify(body)}\n`;
|
|
55
|
-
return Object.freeze({
|
|
56
|
-
status,
|
|
57
|
-
headers: Object.freeze({
|
|
58
|
-
'content-type': 'application/json; charset=utf-8',
|
|
59
|
-
'content-length': String(Buffer.byteLength(encoded)),
|
|
60
|
-
'cache-control': 'no-store',
|
|
61
|
-
...headers,
|
|
62
|
-
}),
|
|
63
|
-
body: encoded,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function sanitizeMessage(input) {
|
|
67
|
-
return [...input].map(character => {
|
|
68
|
-
const code = character.codePointAt(0) ?? 0;
|
|
69
|
-
return code < 0x20 || code === 0x7f ? ' ' : character;
|
|
70
|
-
}).join('').slice(0, 1024).trim() || 'The deployment control-plane request was rejected.';
|
|
71
|
-
}
|
|
72
|
-
function errorResponse(error) {
|
|
73
|
-
const value = error instanceof ControlPlaneError
|
|
74
|
-
? error
|
|
75
|
-
: new ControlPlaneError('HQ_CONTROL_INTERNAL', 'The deployment control-plane request could not be processed.', { expose: false, cause: error });
|
|
76
|
-
return jsonResponse(value.status, {
|
|
77
|
-
error: {
|
|
78
|
-
code: value.code,
|
|
79
|
-
message: sanitizeMessage(value.expose
|
|
80
|
-
? value.message
|
|
81
|
-
: 'The deployment control-plane request could not be processed.'),
|
|
82
|
-
},
|
|
83
|
-
}, value.headers);
|
|
84
|
-
}
|
|
85
|
-
function decodeTargetSegment(input) {
|
|
86
|
-
try {
|
|
87
|
-
return decodeURIComponent(input);
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'Deployment target path encoding is invalid.', { cause: error });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function validatedTarget(project, environment) {
|
|
94
|
-
try {
|
|
95
|
-
return validateProtocolDeploymentReleaseTarget({
|
|
96
|
-
project: decodeTargetSegment(project),
|
|
97
|
-
environment: decodeTargetSegment(environment),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (error instanceof ControlPlaneError)
|
|
102
|
-
throw error;
|
|
103
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'Deployment target path is invalid.', { cause: error });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function parseRoute(path) {
|
|
107
|
-
if (path === '/v1/deployments/submissions')
|
|
108
|
-
return Object.freeze({ kind: 'submission' });
|
|
109
|
-
const segments = path.split('/');
|
|
110
|
-
if (segments.length !== 7 || segments[0] !== '' || segments[1] !== 'v1'
|
|
111
|
-
|| segments[2] !== 'deployments' || segments[3] !== 'targets')
|
|
112
|
-
return undefined;
|
|
113
|
-
if (segments[6] !== 'activation' && segments[6] !== 'activations')
|
|
114
|
-
return undefined;
|
|
115
|
-
const target = validatedTarget(segments[4], segments[5]);
|
|
116
|
-
if (segments[6] === 'activation')
|
|
117
|
-
return Object.freeze({ kind: 'activation', target });
|
|
118
|
-
if (segments[6] === 'activations')
|
|
119
|
-
return Object.freeze({ kind: 'history', target });
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
function requireMethod(method, ...expected) {
|
|
123
|
-
if (!expected.includes(method)) {
|
|
124
|
-
const allowed = expected.join(', ');
|
|
125
|
-
throw new ControlPlaneError('HQ_CONTROL_METHOD_NOT_ALLOWED', `This deployment control-plane route requires ${allowed}.`, { headers: { allow: allowed } });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function requireNoQuery(request) {
|
|
129
|
-
if (request.query && Object.keys(request.query).length > 0) {
|
|
130
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'This deployment control-plane route does not accept query parameters.');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function requireNoBody(request) {
|
|
134
|
-
const length = requestHeader(request.headers, 'content-length');
|
|
135
|
-
const transferEncoding = requestHeader(request.headers, 'transfer-encoding');
|
|
136
|
-
if (request.hasBody || transferEncoding !== undefined || (length !== undefined && length !== '0')) {
|
|
137
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'This deployment control-plane route does not accept a request body.');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function throwIfAborted(signal) {
|
|
141
|
-
if (signal?.aborted) {
|
|
142
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The deployment control-plane request was aborted.', { cause: signal.reason });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function readBoundedBody(request, maximum) {
|
|
146
|
-
const declared = requestHeader(request.headers, 'content-length');
|
|
147
|
-
let declaredLength;
|
|
148
|
-
if (declared !== undefined) {
|
|
149
|
-
if (!/^(?:0|[1-9][0-9]*)$/.test(declared)) {
|
|
150
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'Content-Length must be a non-negative decimal integer.');
|
|
151
|
-
}
|
|
152
|
-
declaredLength = Number(declared);
|
|
153
|
-
if (!Number.isSafeInteger(declaredLength) || declaredLength > maximum) {
|
|
154
|
-
throw new ControlPlaneError('HQ_CONTROL_TOO_LARGE', 'The activation request exceeds its byte limit.');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const chunks = [];
|
|
158
|
-
let total = 0;
|
|
159
|
-
try {
|
|
160
|
-
for await (const chunk of request.body) {
|
|
161
|
-
throwIfAborted(request.signal);
|
|
162
|
-
if (!(chunk instanceof Uint8Array)) {
|
|
163
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request body yielded a non-byte chunk.');
|
|
164
|
-
}
|
|
165
|
-
total += chunk.byteLength;
|
|
166
|
-
if (total > maximum) {
|
|
167
|
-
throw new ControlPlaneError('HQ_CONTROL_TOO_LARGE', 'The activation request exceeds its byte limit.');
|
|
168
|
-
}
|
|
169
|
-
if (declaredLength !== undefined && total > declaredLength) {
|
|
170
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request body does not match Content-Length.');
|
|
171
|
-
}
|
|
172
|
-
chunks.push(Buffer.from(chunk));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
if (error instanceof ControlPlaneError)
|
|
177
|
-
throw error;
|
|
178
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request body could not be read.', { cause: error });
|
|
179
|
-
}
|
|
180
|
-
throwIfAborted(request.signal);
|
|
181
|
-
if (declaredLength !== undefined && total !== declaredLength) {
|
|
182
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request body does not match Content-Length.');
|
|
183
|
-
}
|
|
184
|
-
return Buffer.concat(chunks, total);
|
|
185
|
-
}
|
|
186
|
-
function parseActivationBody(bytes) {
|
|
187
|
-
let input;
|
|
188
|
-
try {
|
|
189
|
-
input = JSON.parse(textDecoder.decode(bytes));
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request must be valid UTF-8 JSON.', { cause: error });
|
|
193
|
-
}
|
|
194
|
-
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
195
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request is invalid.');
|
|
196
|
-
}
|
|
197
|
-
const value = input;
|
|
198
|
-
const expected = ['expectedRevision', 'kind', 'releaseIdentity', 'version'];
|
|
199
|
-
if (Object.keys(value).sort().join('\0') !== expected.join('\0')
|
|
200
|
-
|| value.kind !== 'hypequery-deployment-activation-request' || value.version !== 1
|
|
201
|
-
|| typeof value.releaseIdentity !== 'string'
|
|
202
|
-
|| !IDENTITY_PATTERN.test(value.releaseIdentity)
|
|
203
|
-
|| (value.expectedRevision !== null
|
|
204
|
-
&& (typeof value.expectedRevision !== 'string'
|
|
205
|
-
|| !IDENTITY_PATTERN.test(value.expectedRevision)))) {
|
|
206
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The activation request is invalid.');
|
|
207
|
-
}
|
|
208
|
-
return Object.freeze({
|
|
209
|
-
releaseIdentity: value.releaseIdentity,
|
|
210
|
-
expectedRevision: value.expectedRevision,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
function activationError(error) {
|
|
214
|
-
switch (error.code) {
|
|
215
|
-
case 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST':
|
|
216
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The deployment activation request is invalid.', { cause: error });
|
|
217
|
-
case 'HQ_DEPLOYMENT_ACTIVATION_TARGET_MISMATCH':
|
|
218
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'The deployment release does not match the requested target.', { cause: error });
|
|
219
|
-
case 'HQ_DEPLOYMENT_ACTIVATION_RELEASE_NOT_FOUND':
|
|
220
|
-
throw new ControlPlaneError('HQ_CONTROL_RELEASE_NOT_FOUND', 'The deployment release was not found.', { cause: error });
|
|
221
|
-
case 'HQ_DEPLOYMENT_ACTIVATION_RELEASE_UNAVAILABLE':
|
|
222
|
-
throw new ControlPlaneError('HQ_CONTROL_RELEASE_UNAVAILABLE', 'The deployment release is temporarily unavailable.', { cause: error });
|
|
223
|
-
default:
|
|
224
|
-
throw new ControlPlaneError('HQ_CONTROL_INTERNAL', 'The deployment activation state could not be processed.', { expose: false, cause: error });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function activationCall(operation) {
|
|
228
|
-
try {
|
|
229
|
-
return await operation();
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
if (error instanceof DeploymentActivationError)
|
|
233
|
-
activationError(error);
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function activationResponse(status, activation) {
|
|
238
|
-
return jsonResponse(status === 'activated' ? 201 : 200, {
|
|
239
|
-
kind: 'hypequery-deployment-activation-response',
|
|
240
|
-
version: 1,
|
|
241
|
-
status,
|
|
242
|
-
activation,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
function validatedHistoryQuery(query, maximum) {
|
|
246
|
-
const values = query ?? {};
|
|
247
|
-
if (Object.keys(values).some(key => key !== 'limit' && key !== 'before')) {
|
|
248
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'History query parameters are invalid.');
|
|
249
|
-
}
|
|
250
|
-
const limitInput = values.limit;
|
|
251
|
-
if (limitInput !== undefined && typeof limitInput !== 'string') {
|
|
252
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'History limit is invalid.');
|
|
253
|
-
}
|
|
254
|
-
const limit = limitInput === undefined ? maximum : Number(limitInput);
|
|
255
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum
|
|
256
|
-
|| (limitInput !== undefined && String(limit) !== limitInput)) {
|
|
257
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'History limit is invalid.');
|
|
258
|
-
}
|
|
259
|
-
const before = values.before;
|
|
260
|
-
if (before !== undefined && typeof before !== 'string') {
|
|
261
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'History cursor is invalid.');
|
|
262
|
-
}
|
|
263
|
-
if (before !== undefined) {
|
|
264
|
-
if (!IDENTITY_PATTERN.test(before)) {
|
|
265
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'History cursor is invalid.');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return Object.freeze({ limit, ...(before === undefined ? {} : { before }) });
|
|
269
|
-
}
|
|
270
|
-
export function createDeploymentControlPlane(options) {
|
|
271
|
-
const limits = resolveDeploymentControlPlaneLimits(options.limits);
|
|
272
|
-
async function authenticateAndAuthorize(request, action, target) {
|
|
273
|
-
throwIfAborted(request.signal);
|
|
274
|
-
const token = bearerToken(request.headers);
|
|
275
|
-
const principal = await options.authenticator.authenticate({ token, signal: request.signal });
|
|
276
|
-
if (principal === null) {
|
|
277
|
-
throw new ControlPlaneError('HQ_CONTROL_UNAUTHENTICATED', 'The bearer credential is invalid.', { headers: { 'www-authenticate': 'Bearer' } });
|
|
278
|
-
}
|
|
279
|
-
throwIfAborted(request.signal);
|
|
280
|
-
const allowed = await options.authorizer.authorize({
|
|
281
|
-
principal,
|
|
282
|
-
action,
|
|
283
|
-
target,
|
|
284
|
-
signal: request.signal,
|
|
285
|
-
});
|
|
286
|
-
if (!allowed) {
|
|
287
|
-
throw new ControlPlaneError('HQ_CONTROL_FORBIDDEN', 'The caller is not authorized for the deployment target.');
|
|
288
|
-
}
|
|
289
|
-
throwIfAborted(request.signal);
|
|
290
|
-
}
|
|
291
|
-
async function process(request) {
|
|
292
|
-
const method = request.method.toUpperCase();
|
|
293
|
-
const route = parseRoute(request.path);
|
|
294
|
-
if (!route) {
|
|
295
|
-
throw new ControlPlaneError('HQ_CONTROL_NOT_FOUND', 'Deployment route not found.');
|
|
296
|
-
}
|
|
297
|
-
if (route.kind === 'submission') {
|
|
298
|
-
requireMethod(method, 'POST');
|
|
299
|
-
requireNoQuery(request);
|
|
300
|
-
return options.intake.handle(request);
|
|
301
|
-
}
|
|
302
|
-
const target = route.target;
|
|
303
|
-
if (route.kind === 'activation') {
|
|
304
|
-
if (method === 'GET') {
|
|
305
|
-
requireNoQuery(request);
|
|
306
|
-
requireNoBody(request);
|
|
307
|
-
await authenticateAndAuthorize(request, 'read-current-activation', target);
|
|
308
|
-
const current = await activationCall(() => options.activations.current(target));
|
|
309
|
-
return jsonResponse(200, {
|
|
310
|
-
kind: 'hypequery-deployment-current-activation',
|
|
311
|
-
version: 1,
|
|
312
|
-
target,
|
|
313
|
-
activation: current ?? null,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
requireMethod(method, 'GET', 'PUT');
|
|
317
|
-
requireNoQuery(request);
|
|
318
|
-
await authenticateAndAuthorize(request, 'activate', target);
|
|
319
|
-
const contentType = requestHeader(request.headers, 'content-type');
|
|
320
|
-
if (!contentType || !JSON_CONTENT_TYPE.test(contentType)) {
|
|
321
|
-
throw new ControlPlaneError('HQ_CONTROL_BAD_REQUEST', 'Activation requests require application/json with UTF-8 encoding.');
|
|
322
|
-
}
|
|
323
|
-
const body = parseActivationBody(await readBoundedBody(request, limits.maxActivationRequestBytes));
|
|
324
|
-
const result = await activationCall(() => options.activations.activate({
|
|
325
|
-
target,
|
|
326
|
-
releaseIdentity: body.releaseIdentity,
|
|
327
|
-
expectedRevision: body.expectedRevision,
|
|
328
|
-
}));
|
|
329
|
-
if (result.status === 'conflict') {
|
|
330
|
-
return jsonResponse(409, {
|
|
331
|
-
kind: 'hypequery-deployment-activation-response',
|
|
332
|
-
version: 1,
|
|
333
|
-
status: 'conflict',
|
|
334
|
-
current: result.current,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
return activationResponse(result.status, result.activation);
|
|
338
|
-
}
|
|
339
|
-
requireMethod(method, 'GET');
|
|
340
|
-
requireNoBody(request);
|
|
341
|
-
await authenticateAndAuthorize(request, 'read-activation-history', target);
|
|
342
|
-
const historyQuery = validatedHistoryQuery(request.query, limits.maxHistoryPageSize);
|
|
343
|
-
const page = await activationCall(() => (options.activations.historyPage(target, historyQuery)));
|
|
344
|
-
return jsonResponse(200, {
|
|
345
|
-
kind: 'hypequery-deployment-activation-history',
|
|
346
|
-
version: 1,
|
|
347
|
-
target,
|
|
348
|
-
activations: page.activations,
|
|
349
|
-
nextBefore: page.nextBefore,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return Object.freeze({
|
|
353
|
-
async handle(request) {
|
|
354
|
-
try {
|
|
355
|
-
return await process(request);
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
return errorResponse(error);
|
|
359
|
-
}
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { DeploymentRuntimeFactory } from './runtime-supervisor.js';
|
|
2
|
-
export type NodeDeploymentRuntimeErrorCode = 'HQ_NODE_RUNTIME_CONFIGURATION' | 'HQ_NODE_RUNTIME_UNSUPPORTED_ARTIFACT' | 'HQ_NODE_RUNTIME_INVALID_ARTIFACT' | 'HQ_NODE_RUNTIME_START_FAILED' | 'HQ_NODE_RUNTIME_WORKER_EXITED' | 'HQ_NODE_RUNTIME_INVOCATION_FAILED' | 'HQ_NODE_RUNTIME_ABORTED' | 'HQ_NODE_RUNTIME_CLOSED';
|
|
3
|
-
export declare class NodeDeploymentRuntimeError extends Error {
|
|
4
|
-
readonly code: NodeDeploymentRuntimeErrorCode;
|
|
5
|
-
constructor(code: NodeDeploymentRuntimeErrorCode, message: string, options?: {
|
|
6
|
-
readonly cause?: unknown;
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
export interface NodeDeploymentRuntimeFactoryOptions {
|
|
10
|
-
readonly temporaryDirectory?: string;
|
|
11
|
-
/** Worker import deadline from 1 through 300,000 milliseconds. */
|
|
12
|
-
readonly startupTimeoutMs?: number;
|
|
13
|
-
readonly onCleanupError?: (error: unknown, directory: string) => void;
|
|
14
|
-
}
|
|
15
|
-
export declare function createNodeWorkerDeploymentRuntimeFactory(options?: NodeDeploymentRuntimeFactoryOptions): DeploymentRuntimeFactory;
|
|
16
|
-
//# sourceMappingURL=node-runtime-factory.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"node-runtime-factory.d.ts","sourceRoot":"","sources":["../src/node-runtime-factory.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,wBAAwB,EAGzB,MAAM,yBAAyB,CAAC;AA+CjC,MAAM,MAAM,8BAA8B,GACtC,+BAA+B,GAC/B,sCAAsC,GACtC,kCAAkC,GAClC,8BAA8B,GAC9B,+BAA+B,GAC/B,mCAAmC,GACnC,yBAAyB,GACzB,wBAAwB,CAAC;AAE7B,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAE,8BAA8B,CAAC;gBAG5C,IAAI,EAAE,8BAA8B,EACpC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAM7C;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACvE;AAoSD,wBAAgB,wCAAwC,CACtD,OAAO,GAAE,mCAAwC,GAChD,wBAAwB,CAuB1B"}
|