@hypequery/deployment 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @hypequery/deployment
2
2
 
3
- Provider-neutral verification and receiving-side intake for Hypequery deployment
4
- bundles.
3
+ Provider-neutral verification, intake, activation, and HTTP control-plane
4
+ building blocks for Hypequery deployment bundles.
5
5
 
6
6
  The package accepts the authenticated multipart transport emitted by
7
7
  `hypequery deploy`, reconstructs only manifest-declared files in temporary
@@ -152,4 +152,97 @@ append-only chain. It has no mutable pointer file or persistent lock to become
152
152
  stale after a crash. Activation does not load runtime code, route traffic,
153
153
  perform health checks, or authorize callers; those remain provider concerns.
154
154
 
155
+ ## HTTP control plane
156
+
157
+ `createDeploymentControlPlane` combines intake and activation behind closed v1
158
+ routes. Activation reads and writes use a separate target-scoped authorizer;
159
+ write authorization completes before the small JSON request body is consumed.
160
+ The Fetch and Node adapters preserve streaming multipart submission bodies.
161
+
162
+ ```ts
163
+ import {
164
+ createDeploymentControlPlane,
165
+ createDeploymentControlPlaneNodeHandler,
166
+ } from '@hypequery/deployment';
167
+
168
+ const controlPlane = createDeploymentControlPlane({
169
+ intake,
170
+ activations,
171
+ authenticator,
172
+ authorizer: {
173
+ async authorize({ principal, action, target }) {
174
+ return canControlDeployment(principal, action, target);
175
+ },
176
+ },
177
+ });
178
+
179
+ const nodeHandler = createDeploymentControlPlaneNodeHandler(controlPlane);
180
+ ```
181
+
182
+ The control plane exposes release submission, compare-and-swap activation,
183
+ current state, and bounded cursor history. It returns stable, bounded JSON error
184
+ codes and suppresses internal provider and filesystem details. The HTTP
185
+ contract is specified in `specs/deployment/0003-control-plane-http.md`.
186
+
187
+ ## Runtime materialization
188
+
189
+ `createDeploymentRuntimeMaterializer` converts the current target activation
190
+ into a private runtime snapshot. It revalidates the accepted release and closed
191
+ bundle, copies and hashes each runtime artifact without following symbolic
192
+ links, and confirms the activation revision again before returning.
193
+
194
+ ```ts
195
+ import { createDeploymentRuntimeMaterializer } from '@hypequery/deployment';
196
+
197
+ const materializer = createDeploymentRuntimeMaterializer({
198
+ activations,
199
+ releases: store,
200
+ });
201
+
202
+ const snapshot = await materializer.current({
203
+ project: 'analytics',
204
+ environment: 'production',
205
+ });
206
+ ```
207
+
208
+ Artifact `read()` methods return fresh byte copies, so neither callers nor later
209
+ changes to durable storage can alter a materialized snapshot. Runtime imports,
210
+ process lifecycle, readiness, and traffic switching remain separate concerns.
211
+
212
+ ## Runtime supervision
213
+
214
+ `createDeploymentRuntimeSupervisor` starts materialized snapshots through a
215
+ runtime factory, checks candidate readiness, confirms activation again, and
216
+ atomically changes the generation used for new named-query invocations. Failed
217
+ or superseded candidates never displace a healthy generation.
218
+
219
+ ```ts
220
+ import {
221
+ createDeploymentRuntimeSupervisor,
222
+ createNodeWorkerDeploymentRuntimeFactory,
223
+ } from '@hypequery/deployment';
224
+
225
+ const supervisor = createDeploymentRuntimeSupervisor({
226
+ materializer,
227
+ factory: createNodeWorkerDeploymentRuntimeFactory(),
228
+ });
229
+
230
+ await supervisor.reconcile({ project: 'analytics', environment: 'production' });
231
+ const result = await supervisor.invoke({
232
+ target: { project: 'analytics', environment: 'production' },
233
+ query: 'orders',
234
+ argument: { input, ctx: { tenantId } },
235
+ });
236
+ ```
237
+
238
+ Calls already assigned to an old generation may finish after cutover. That
239
+ generation receives no new calls and closes when its in-flight work reaches
240
+ zero or the drain deadline expires. The reference Node factory loads exact
241
+ materialized bytes in worker threads and removes their temporary files on
242
+ shutdown. Provider factories can implement Python, process, container, or
243
+ remote-sandbox isolation behind the same lifecycle interface.
244
+
245
+ The reference worker is a lifecycle boundary, not a hostile-code security
246
+ sandbox. Only trusted deployment code should use it directly.
247
+
155
248
  The package is ESM-only and requires Node.js 20 or newer.
@@ -29,6 +29,14 @@ export type DeploymentActivationResult = {
29
29
  readonly status: 'conflict';
30
30
  readonly current: DeploymentActivationRecord | null;
31
31
  };
32
+ export interface DeploymentActivationHistoryQuery {
33
+ readonly limit: number;
34
+ readonly before?: string;
35
+ }
36
+ export interface DeploymentActivationHistoryPage {
37
+ readonly activations: readonly DeploymentActivationRecord[];
38
+ readonly nextBefore: string | null;
39
+ }
32
40
  export interface DeploymentActivationRelease {
33
41
  readonly release: ProtocolDeploymentReleaseEnvelope;
34
42
  readonly releaseIdentity: string;
@@ -46,6 +54,9 @@ export interface DeploymentActivationRegistry {
46
54
  activate(request: DeploymentActivationRequest): Promise<DeploymentActivationResult>;
47
55
  current(target: ProtocolDeploymentReleaseTarget): Promise<DeploymentActivationRecord | undefined>;
48
56
  history(target: ProtocolDeploymentReleaseTarget): Promise<readonly DeploymentActivationRecord[]>;
57
+ historyPage(target: ProtocolDeploymentReleaseTarget, query: DeploymentActivationHistoryQuery): Promise<DeploymentActivationHistoryPage>;
49
58
  }
59
+ /** Validate and recompute an immutable deployment activation record. */
60
+ export declare function validateDeploymentActivationRecord(input: unknown): DeploymentActivationRecord;
50
61
  export declare function createFileSystemDeploymentActivationRegistry(options: FileSystemDeploymentActivationRegistryOptions): DeploymentActivationRegistry;
51
62
  //# sourceMappingURL=activation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"activation.d.ts","sourceRoot":"","sources":["../src/activation.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,iCAAiC,EACtC,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAe7B,MAAM,MAAM,6BAA6B,GACrC,wCAAwC,GACxC,0CAA0C,GAC1C,4CAA4C,GAC5C,8CAA8C,GAC9C,0CAA0C,GAC1C,wCAAwC,GACxC,6BAA6B,CAAC;AAElC,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;gBAG3C,IAAI,EAAE,6BAA6B,EACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAM7C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,iCAAiC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,MAAM,0BAA0B,GAClC;IACA,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,0BAA0B,CAAC;CACjD,GACC;IACA,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAC;CACrD,CAAC;AAEJ,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,iCAAiC,CAAC;IACpD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,6FAA6F;IAC7F,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC,CAAC;CACjF;AAED,MAAM,WAAW,6CAA6C;IAC5D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACpF,OAAO,CACL,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAAC;IACnD,OAAO,CACL,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,SAAS,0BAA0B,EAAE,CAAC,CAAC;CACnD;AAqdD,wBAAgB,4CAA4C,CAC1D,OAAO,EAAE,6CAA6C,GACrD,4BAA4B,CAqM9B"}
1
+ {"version":3,"file":"activation.d.ts","sourceRoot":"","sources":["../src/activation.ts"],"names":[],"mappings":"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"}
@@ -128,21 +128,16 @@ function requireRecord(input) {
128
128
  }
129
129
  return value;
130
130
  }
131
- function validateStoredActivation(input, expectedTarget, expectedPreviousRevision, expectedPreviousReleaseIdentity) {
131
+ function prepareValidatedActivation(input) {
132
132
  const value = requireRecord(input);
133
133
  if (value.kind !== 'hypequery-deployment-activation' || value.version !== 1) {
134
134
  throw new Error('Activation record kind or version is invalid.');
135
135
  }
136
136
  const target = validateTarget(value.target, 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
137
- if (!targetsEqual(target, expectedTarget)) {
138
- throw new Error('Activation record target is inconsistent.');
139
- }
140
137
  const releaseIdentity = requireIdentity(value.releaseIdentity, 'Stored release identity', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
141
138
  const previousRevision = requireNullableIdentity(value.previousRevision, 'Stored previous revision', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
142
139
  const previousReleaseIdentity = requireNullableIdentity(value.previousReleaseIdentity, 'Stored previous release identity', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
143
- if (previousRevision !== expectedPreviousRevision
144
- || previousReleaseIdentity !== expectedPreviousReleaseIdentity
145
- || (previousRevision === null) !== (previousReleaseIdentity === null)) {
140
+ if ((previousRevision === null) !== (previousReleaseIdentity === null)) {
146
141
  throw new Error('Activation record predecessor is inconsistent.');
147
142
  }
148
143
  if (typeof value.activatedAt !== 'string') {
@@ -164,6 +159,21 @@ function validateStoredActivation(input, expectedTarget, expectedPreviousRevisio
164
159
  }
165
160
  return prepared;
166
161
  }
162
+ /** Validate and recompute an immutable deployment activation record. */
163
+ export function validateDeploymentActivationRecord(input) {
164
+ return prepareValidatedActivation(input).record;
165
+ }
166
+ function validateStoredActivation(input, expectedTarget, expectedPreviousRevision, expectedPreviousReleaseIdentity) {
167
+ const prepared = prepareValidatedActivation(input);
168
+ if (!targetsEqual(prepared.record.target, expectedTarget)) {
169
+ throw new Error('Activation record target is inconsistent.');
170
+ }
171
+ if (prepared.record.previousRevision !== expectedPreviousRevision
172
+ || prepared.record.previousReleaseIdentity !== expectedPreviousReleaseIdentity) {
173
+ throw new Error('Activation record predecessor is inconsistent.');
174
+ }
175
+ return prepared;
176
+ }
167
177
  async function pathExists(filePath) {
168
178
  try {
169
179
  await lstat(filePath);
@@ -335,7 +345,7 @@ async function readStoredActivation(claimDirectory, target, previousRevision, pr
335
345
  }
336
346
  return prepared;
337
347
  }
338
- async function resolveHistory(targetDirectory, target) {
348
+ async function visitHistory(targetDirectory, target, visitor) {
339
349
  try {
340
350
  await readCanonicalTarget(targetDirectory, target);
341
351
  const claimsDirectory = path.join(targetDirectory, CLAIMS_DIRECTORY);
@@ -347,7 +357,6 @@ async function resolveHistory(targetDirectory, target) {
347
357
  }
348
358
  const remaining = new Set(claimNames);
349
359
  const revisions = new Set();
350
- const history = [];
351
360
  let previousRevision = null;
352
361
  let previousReleaseIdentity = null;
353
362
  // Following predecessor-named claims derives the head without trusting a
@@ -361,14 +370,13 @@ async function resolveHistory(targetDirectory, target) {
361
370
  throw new Error('Deployment activation history contains a cycle.');
362
371
  }
363
372
  revisions.add(prepared.record.revision);
364
- history.push(prepared.record);
373
+ visitor(prepared.record);
365
374
  previousRevision = prepared.record.revision;
366
375
  previousReleaseIdentity = prepared.record.releaseIdentity;
367
376
  }
368
377
  if (remaining.size > 0) {
369
378
  throw new Error('Deployment activation history contains unreachable claims.');
370
379
  }
371
- return Object.freeze(history);
372
380
  }
373
381
  catch (error) {
374
382
  if (error instanceof DeploymentActivationError)
@@ -378,8 +386,53 @@ async function resolveHistory(targetDirectory, target) {
378
386
  throw activationError('HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE', 'Stored deployment activation history is invalid.', error);
379
387
  }
380
388
  }
381
- function currentActivation(history) {
382
- return history[history.length - 1];
389
+ async function resolveHistory(targetDirectory, target) {
390
+ const history = [];
391
+ await visitHistory(targetDirectory, target, record => { history.push(record); });
392
+ return Object.freeze(history);
393
+ }
394
+ async function resolveCurrent(targetDirectory, target) {
395
+ let current;
396
+ await visitHistory(targetDirectory, target, record => { current = record; });
397
+ return current;
398
+ }
399
+ function validateHistoryQuery(input) {
400
+ if (!Number.isSafeInteger(input?.limit) || input.limit < 1
401
+ || (input.before !== undefined && !IDENTITY_PATTERN.test(input.before))) {
402
+ throw activationError('HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST', 'Deployment activation history query is invalid.');
403
+ }
404
+ return Object.freeze({
405
+ limit: input.limit,
406
+ ...(input.before === undefined ? {} : { before: input.before }),
407
+ });
408
+ }
409
+ async function resolveHistoryPage(targetDirectory, target, query) {
410
+ const retained = [];
411
+ let beforeFound = query.before === undefined;
412
+ let beforeReached = false;
413
+ await visitHistory(targetDirectory, target, record => {
414
+ if (record.revision === query.before) {
415
+ beforeFound = true;
416
+ beforeReached = true;
417
+ return;
418
+ }
419
+ if (beforeReached)
420
+ return;
421
+ retained.push(record);
422
+ if (retained.length > query.limit + 1)
423
+ retained.shift();
424
+ });
425
+ if (!beforeFound) {
426
+ throw activationError('HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST', 'Deployment activation history cursor was not found.');
427
+ }
428
+ const hasOlder = retained.length > query.limit;
429
+ if (hasOlder)
430
+ retained.shift();
431
+ const activations = Object.freeze(retained);
432
+ return Object.freeze({
433
+ activations,
434
+ nextBefore: hasOlder ? activations[0].revision : null,
435
+ });
383
436
  }
384
437
  export function createFileSystemDeploymentActivationRegistry(options) {
385
438
  if (typeof options.directory !== 'string' || options.directory.length < 1) {
@@ -433,8 +486,37 @@ export function createFileSystemDeploymentActivationRegistry(options) {
433
486
  throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read deployment activation history.', error);
434
487
  }
435
488
  }
436
- async function current(target) {
437
- return currentActivation(await history(target));
489
+ async function current(targetInput) {
490
+ const target = validateTarget(targetInput, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
491
+ try {
492
+ const key = sha256(TARGET_IDENTITY_DOMAIN, targetCanonical(target));
493
+ const targetDirectory = path.join(await activationRoot(), key);
494
+ if (!await pathExists(targetDirectory))
495
+ return undefined;
496
+ return await resolveCurrent(targetDirectory, target);
497
+ }
498
+ catch (error) {
499
+ if (error instanceof DeploymentActivationError)
500
+ throw error;
501
+ throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read the current deployment activation.', error);
502
+ }
503
+ }
504
+ async function historyPage(targetInput, queryInput) {
505
+ const target = validateTarget(targetInput, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
506
+ const query = validateHistoryQuery(queryInput);
507
+ try {
508
+ const key = sha256(TARGET_IDENTITY_DOMAIN, targetCanonical(target));
509
+ const targetDirectory = path.join(await activationRoot(), key);
510
+ if (!await pathExists(targetDirectory)) {
511
+ return Object.freeze({ activations: Object.freeze([]), nextBefore: null });
512
+ }
513
+ return await resolveHistoryPage(targetDirectory, target, query);
514
+ }
515
+ catch (error) {
516
+ if (error instanceof DeploymentActivationError)
517
+ throw error;
518
+ throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read deployment activation history.', error);
519
+ }
438
520
  }
439
521
  async function activate(request) {
440
522
  const target = validateTarget(request.target, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
@@ -467,8 +549,7 @@ export function createFileSystemDeploymentActivationRegistry(options) {
467
549
  try {
468
550
  const activations = await activationRoot();
469
551
  const targetDirectory = await ensureTargetDirectory(activations, target);
470
- const existingHistory = await resolveHistory(targetDirectory, target);
471
- const existing = currentActivation(existingHistory);
552
+ const existing = await resolveCurrent(targetDirectory, target);
472
553
  if (existing?.releaseIdentity === releaseIdentity) {
473
554
  return Object.freeze({ status: 'already-active', activation: existing });
474
555
  }
@@ -514,7 +595,7 @@ export function createFileSystemDeploymentActivationRegistry(options) {
514
595
  if (published) {
515
596
  return Object.freeze({ status: 'activated', activation: prepared.record });
516
597
  }
517
- const resolved = currentActivation(await resolveHistory(targetDirectory, target));
598
+ const resolved = await resolveCurrent(targetDirectory, target);
518
599
  if (resolved?.releaseIdentity === releaseIdentity) {
519
600
  return Object.freeze({ status: 'already-active', activation: resolved });
520
601
  }
@@ -526,5 +607,5 @@ export function createFileSystemDeploymentActivationRegistry(options) {
526
607
  throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not update deployment activation state.', error);
527
608
  }
528
609
  }
529
- return Object.freeze({ activate, current, history });
610
+ return Object.freeze({ activate, current, history, historyPage });
530
611
  }
@@ -0,0 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { DeploymentControlPlane } from './control-plane.js';
3
+ export type DeploymentControlPlaneFetchHandler = (request: Request) => Promise<Response>;
4
+ export type DeploymentControlPlaneNodeHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
5
+ export declare function createDeploymentControlPlaneFetchHandler(controlPlane: DeploymentControlPlane): DeploymentControlPlaneFetchHandler;
6
+ export declare function createDeploymentControlPlaneNodeHandler(controlPlane: DeploymentControlPlane): DeploymentControlPlaneNodeHandler;
7
+ //# sourceMappingURL=control-plane-adapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-plane-adapters.d.ts","sourceRoot":"","sources":["../src/control-plane-adapters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EACV,sBAAsB,EAGvB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kCAAkC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AACzF,MAAM,MAAM,iCAAiC,GAAG,CAC9C,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,KACrB,OAAO,CAAC,IAAI,CAAC,CAAC;AAkHnB,wBAAgB,wCAAwC,CACtD,YAAY,EAAE,sBAAsB,GACnC,kCAAkC,CAkBpC;AAED,wBAAgB,uCAAuC,CACrD,YAAY,EAAE,sBAAsB,GACnC,iCAAiC,CAgCnC"}
@@ -0,0 +1,163 @@
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
+ }
@@ -0,0 +1,7 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,35 @@
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
@@ -0,0 +1 @@
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"}