@hypequery/deployment 0.0.0-canary-20260719200737 → 0.1.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.
@@ -1,163 +0,0 @@
1
- const INTERNAL_BODY = '{"error":{"code":"HQ_CONTROL_INTERNAL","message":"The deployment control-plane request could not be processed."}}\n';
2
- const INTERNAL_RESPONSE = Object.freeze({
3
- status: 500,
4
- headers: Object.freeze({
5
- 'content-type': 'application/json; charset=utf-8',
6
- 'content-length': String(Buffer.byteLength(INTERNAL_BODY)),
7
- 'cache-control': 'no-store',
8
- }),
9
- body: INTERNAL_BODY,
10
- });
11
- const FETCH_SINGLETON_HEADERS = new Set([
12
- 'authorization',
13
- 'content-length',
14
- 'content-type',
15
- 'idempotency-key',
16
- 'x-hypequery-bundle-identity',
17
- 'x-hypequery-release-identity',
18
- ]);
19
- function queryParameters(search) {
20
- const query = Object.create(null);
21
- for (const [name, value] of search) {
22
- const current = query[name];
23
- if (current === undefined) {
24
- query[name] = value;
25
- }
26
- else if (typeof current === 'string') {
27
- query[name] = Object.freeze([current, value]);
28
- }
29
- else {
30
- query[name] = Object.freeze([...current, value]);
31
- }
32
- }
33
- return Object.freeze(query);
34
- }
35
- function fetchHeaders(input) {
36
- const headers = Object.create(null);
37
- for (const [name, value] of input.entries()) {
38
- headers[name] = value;
39
- if (FETCH_SINGLETON_HEADERS.has(name) && value.includes(',')) {
40
- // Fetch combines duplicate header lines before exposing Headers. Reify a
41
- // second case-insensitive entry so the core singleton guard rejects it.
42
- headers[name.toUpperCase()] = value;
43
- }
44
- }
45
- return Object.freeze(headers);
46
- }
47
- async function* fetchBody(input) {
48
- if (input === null)
49
- return;
50
- const reader = input.getReader();
51
- try {
52
- while (true) {
53
- const result = await reader.read();
54
- if (result.done)
55
- return;
56
- yield result.value;
57
- }
58
- }
59
- finally {
60
- reader.releaseLock();
61
- }
62
- }
63
- function nodeHeaders(request) {
64
- const headers = Object.create(null);
65
- for (let index = 0; index < request.rawHeaders.length; index += 2) {
66
- const name = request.rawHeaders[index].toLowerCase();
67
- const value = request.rawHeaders[index + 1];
68
- if (headers[name] === undefined) {
69
- headers[name] = value;
70
- }
71
- else {
72
- // Keep a second case-insensitive entry so the core duplicate-header guard
73
- // observes duplicates instead of accepting Node's comma-joined value.
74
- headers[name.toUpperCase()] = value;
75
- }
76
- }
77
- return Object.freeze(headers);
78
- }
79
- async function* nodeBody(request) {
80
- for await (const chunk of request) {
81
- yield chunk instanceof Uint8Array ? chunk : Buffer.from(chunk);
82
- }
83
- }
84
- function nodeHasBody(headers) {
85
- return Object.entries(headers).some(([name, value]) => {
86
- const normalized = name.toLowerCase();
87
- return normalized === 'transfer-encoding'
88
- || (normalized === 'content-length' && value !== '0');
89
- });
90
- }
91
- function internalFetchResponse() {
92
- return new Response(INTERNAL_RESPONSE.body, {
93
- status: INTERNAL_RESPONSE.status,
94
- headers: INTERNAL_RESPONSE.headers,
95
- });
96
- }
97
- function sendNodeResponse(response, controlResponse) {
98
- if (response.writableEnded || response.headersSent)
99
- return;
100
- response.statusCode = controlResponse.status;
101
- for (const [name, value] of Object.entries(controlResponse.headers)) {
102
- response.setHeader(name, value);
103
- }
104
- response.end(controlResponse.body);
105
- }
106
- export function createDeploymentControlPlaneFetchHandler(controlPlane) {
107
- return async (request) => {
108
- try {
109
- const url = new URL(request.url);
110
- const response = await controlPlane.handle({
111
- method: request.method,
112
- path: url.pathname,
113
- query: queryParameters(url.searchParams),
114
- headers: fetchHeaders(request.headers),
115
- body: fetchBody(request.body),
116
- hasBody: request.body !== null,
117
- signal: request.signal,
118
- });
119
- return new Response(response.body, { status: response.status, headers: response.headers });
120
- }
121
- catch {
122
- return internalFetchResponse();
123
- }
124
- };
125
- }
126
- export function createDeploymentControlPlaneNodeHandler(controlPlane) {
127
- return async (request, response) => {
128
- const abort = new AbortController();
129
- const abortRequest = () => {
130
- if (!request.complete)
131
- abort.abort(new Error('The request connection was closed.'));
132
- };
133
- const abortResponse = () => {
134
- if (!response.writableEnded)
135
- abort.abort(new Error('The response connection was closed.'));
136
- };
137
- request.socket.once('close', abortRequest);
138
- response.once('close', abortResponse);
139
- if (request.destroyed && !request.complete)
140
- abortRequest();
141
- try {
142
- const url = new URL(request.url ?? '/', 'http://deployment-control-plane.invalid');
143
- const headers = nodeHeaders(request);
144
- const controlResponse = await controlPlane.handle({
145
- method: request.method ?? 'GET',
146
- path: url.pathname,
147
- query: queryParameters(url.searchParams),
148
- headers,
149
- body: nodeBody(request),
150
- hasBody: nodeHasBody(headers),
151
- signal: abort.signal,
152
- });
153
- sendNodeResponse(response, controlResponse);
154
- }
155
- catch {
156
- sendNodeResponse(response, INTERNAL_RESPONSE);
157
- }
158
- finally {
159
- request.socket.off('close', abortRequest);
160
- response.off('close', abortResponse);
161
- }
162
- };
163
- }
@@ -1,7 +0,0 @@
1
- export interface DeploymentControlPlaneLimits {
2
- readonly maxActivationRequestBytes: number;
3
- readonly maxHistoryPageSize: number;
4
- }
5
- export declare const DEFAULT_DEPLOYMENT_CONTROL_PLANE_LIMITS: Readonly<DeploymentControlPlaneLimits>;
6
- export declare function resolveDeploymentControlPlaneLimits(input?: Partial<DeploymentControlPlaneLimits>): Readonly<DeploymentControlPlaneLimits>;
7
- //# sourceMappingURL=control-plane-limits.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"control-plane-limits.d.ts","sourceRoot":"","sources":["../src/control-plane-limits.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,eAAO,MAAM,uCAAuC,EACpD,QAAQ,CAAC,4BAA4B,CAGnC,CAAC;AAEH,wBAAgB,mCAAmC,CACjD,KAAK,GAAE,OAAO,CAAC,4BAA4B,CAAM,GAChD,QAAQ,CAAC,4BAA4B,CAAC,CAcxC"}
@@ -1,18 +0,0 @@
1
- export const DEFAULT_DEPLOYMENT_CONTROL_PLANE_LIMITS = Object.freeze({
2
- maxActivationRequestBytes: 16 * 1024,
3
- maxHistoryPageSize: 100,
4
- });
5
- export function resolveDeploymentControlPlaneLimits(input = {}) {
6
- const limits = { ...DEFAULT_DEPLOYMENT_CONTROL_PLANE_LIMITS };
7
- for (const key of Object.keys(limits)) {
8
- const value = input[key];
9
- if (value === undefined)
10
- continue;
11
- if (!Number.isSafeInteger(value) || value < 1
12
- || value > DEFAULT_DEPLOYMENT_CONTROL_PLANE_LIMITS[key]) {
13
- throw new RangeError(`${key} must be a positive safe integer no greater than the control-plane v1 maximum`);
14
- }
15
- limits[key] = value;
16
- }
17
- return Object.freeze(limits);
18
- }
@@ -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"}
@@ -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,25 +0,0 @@
1
- import { type ProtocolDeploymentReleaseEnvelope } from '@hypequery/protocol';
2
- import { type VerifiedDeploymentBundle } from './bundle.js';
3
- import type { DeploymentSubmissionStore } from './types.js';
4
- export type FileSystemDeploymentStoreErrorCode = 'HQ_DEPLOYMENT_STORE_CONFIGURATION' | 'HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION' | 'HQ_DEPLOYMENT_STORE_CORRUPT_STATE' | 'HQ_DEPLOYMENT_STORE_IO';
5
- export declare class FileSystemDeploymentStoreError extends Error {
6
- readonly code: FileSystemDeploymentStoreErrorCode;
7
- constructor(code: FileSystemDeploymentStoreErrorCode, message: string, options?: {
8
- readonly cause?: unknown;
9
- });
10
- }
11
- export interface FileSystemDeploymentSubmissionStoreOptions {
12
- readonly directory: string;
13
- }
14
- export interface StoredDeploymentSubmission {
15
- readonly releaseDirectory: string;
16
- readonly release: ProtocolDeploymentReleaseEnvelope;
17
- readonly releaseCanonical: string;
18
- readonly releaseIdentity: string;
19
- readonly bundle: VerifiedDeploymentBundle;
20
- }
21
- export interface FileSystemDeploymentSubmissionStore<Principal> extends DeploymentSubmissionStore<Principal> {
22
- read(releaseIdentity: string): Promise<StoredDeploymentSubmission | undefined>;
23
- }
24
- export declare function createFileSystemDeploymentSubmissionStore<Principal = unknown>(options: FileSystemDeploymentSubmissionStoreOptions): FileSystemDeploymentSubmissionStore<Principal>;
25
- //# sourceMappingURL=filesystem-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"filesystem-store.d.ts","sourceRoot":"","sources":["../src/filesystem-store.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,YAAY,CAAC;AASpB,MAAM,MAAM,kCAAkC,GAC1C,mCAAmC,GACnC,wCAAwC,GACxC,mCAAmC,GACnC,wBAAwB,CAAC;AAE7B,qBAAa,8BAA+B,SAAQ,KAAK;IACvD,QAAQ,CAAC,IAAI,EAAE,kCAAkC,CAAC;gBAGhD,IAAI,EAAE,kCAAkC,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAM7C;AAED,MAAM,WAAW,0CAA0C;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,iCAAiC,CAAC;IACpD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC3C;AAED,MAAM,WAAW,mCAAmC,CAAC,SAAS,CAC5D,SAAQ,yBAAyB,CAAC,SAAS,CAAC;IAC5C,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAAC;CAChF;AAobD,wBAAgB,yCAAyC,CAAC,SAAS,GAAG,OAAO,EAC3E,OAAO,EAAE,0CAA0C,GAClD,mCAAmC,CAAC,SAAS,CAAC,CAkHhD"}