@hypequery/deployment 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +51 -332
  2. package/package.json +16 -4
package/README.md CHANGED
@@ -1,371 +1,90 @@
1
1
  # @hypequery/deployment
2
2
 
3
- Provider-neutral verification, intake, activation, runtime, and HTTP hosting
4
- building blocks for Hypequery deployment bundles.
3
+ Provider-neutral building blocks for receiving, verifying, activating, and hosting Hypequery deployment bundles.
5
4
 
6
- The package accepts the authenticated multipart transport emitted by
7
- `hypequery deploy`, reconstructs only manifest-declared files in temporary
8
- storage, and revalidates the release, bundle manifest, file hashes, deployment
9
- identity, and runtime references before handing the submission to a store.
5
+ This package is for Cloud providers and self-hosted control planes. Application teams normally use `hypequery deploy` through `@hypequery/cli` instead.
10
6
 
11
- ## Intake adapters
7
+ ## What it protects
12
8
 
13
- `createDeploymentIntake` is independent of an HTTP framework. Adapt an incoming
14
- request into a case-insensitive header record and an `AsyncIterable<Uint8Array>`
15
- body, then map the returned status, headers, and JSON body onto the framework's
16
- response.
9
+ Every deployment is treated as immutable content. Before storage or execution, the package verifies:
17
10
 
18
- Three provider-owned adapters are required:
11
+ - the target-bound release envelope;
12
+ - the closed bundle manifest;
13
+ - declared paths and byte limits;
14
+ - every file hash and artifact reference;
15
+ - deployment, bundle, and release identities;
16
+ - activation revision consistency.
19
17
 
20
- - `DeploymentAuthenticator` validates the bearer token before request bytes are
21
- consumed;
22
- - `DeploymentAuthorizer` authorizes the canonical project and environment once
23
- the release envelope has been validated;
24
- - `DeploymentSubmissionStore` atomically persists or recognizes the fully
25
- verified release identity.
18
+ Symbolic links, undeclared files, path traversal, missing content, and identity mismatches fail closed.
26
19
 
27
- The store receives a verified bundle whose directory is temporary. It must copy
28
- or persist every required byte before `accept` resolves. Returning
29
- `already-exists` is valid only for an authorized replay of the same deterministic
30
- release identity.
20
+ ## Provider building blocks
21
+
22
+ - streaming authenticated intake;
23
+ - provider-owned authentication, authorization, and storage interfaces;
24
+ - a durable reference filesystem store;
25
+ - compare-and-swap activation and rollback;
26
+ - Node and Fetch control-plane adapters;
27
+ - immutable runtime materialization;
28
+ - readiness-gated runtime supervision;
29
+ - named-query data-plane execution;
30
+ - a reference single-host composition.
31
+
32
+ ## Minimal intake
31
33
 
32
34
  ```ts
33
35
  import { createDeploymentIntake } from '@hypequery/deployment';
34
36
 
35
37
  const intake = createDeploymentIntake({
36
38
  authenticator: {
37
- async authenticate({ token }) {
38
- return authenticateToken(token);
39
- },
39
+ authenticate: ({ token }) => authenticateToken(token),
40
40
  },
41
41
  authorizer: {
42
- async authorize({ principal, target }) {
43
- return canDeploy(principal, target.project, target.environment);
44
- },
42
+ authorize: ({ principal, target }) =>
43
+ canDeploy(principal, target),
45
44
  },
46
45
  store: {
47
- async accept(submission) {
48
- return persistVerifiedSubmission(submission);
49
- },
46
+ accept: (submission) => persistVerifiedSubmission(submission),
50
47
  },
51
48
  });
52
49
  ```
53
50
 
54
- New submissions return HTTP status `202` and `status: "accepted"`; verified
55
- idempotent replays return HTTP status `200` and `status: "already-exists"`.
56
- Malformed, unauthorized, oversized, or inconsistent submissions fail closed
57
- with a bounded JSON error response. Temporary upload data is removed on success
58
- and failure.
59
-
60
- ## Bundle verification
51
+ Authentication happens before upload bytes are consumed. The store receives a fully verified temporary bundle and must persist required bytes before returning.
61
52
 
62
- `verifyDeploymentBundle(directory)` verifies a closed bundle directly from the
63
- filesystem. It rejects symbolic links and undeclared entries, enforces byte
64
- limits, recomputes every hash and identity, and returns an immutable verified
65
- snapshot.
66
-
67
- ## Filesystem store
68
-
69
- `createFileSystemDeploymentSubmissionStore` is the reference durable store for
70
- a single host. It copies verified bundle bytes out of temporary intake storage,
71
- revalidates the copy, and atomically publishes the release only after its bundle
72
- is durable. Replaying the same release returns `already-exists` after the stored
73
- state has been fully revalidated.
53
+ ## Single-host reference
74
54
 
75
55
  ```ts
76
- import {
77
- createDeploymentIntake,
78
- createFileSystemDeploymentSubmissionStore,
79
- } from '@hypequery/deployment';
56
+ import { createFileSystemDeploymentHost } from '@hypequery/deployment';
80
57
 
81
- const store = createFileSystemDeploymentSubmissionStore({
58
+ const service = createFileSystemDeploymentHost({
82
59
  directory: '/var/lib/hypequery/deployments',
83
- });
84
-
85
- const intake = createDeploymentIntake({
86
- authenticator,
87
- authorizer,
88
- store,
89
- });
90
- ```
91
-
92
- The closed layout is content-addressed:
93
-
94
- ```text
95
- <directory>/
96
- bundles/<bundle identity>/...
97
- releases/<release identity>/release.json
98
- ```
99
-
100
- Bundle directories are published before release directories. A crash can leave
101
- an unreferenced bundle that a later submission safely reuses, but cannot expose
102
- a release whose bundle is incomplete. Files and directories are opened without
103
- following symbolic links where Node exposes that facility, file contents and
104
- directory entries are revalidated on reads and replays, and temporary staging
105
- directories are removed after success or failure.
106
-
107
- The configured directory is an operator-controlled local trust boundary. This
108
- store does not provide remote replication, activation, lifecycle state, or
109
- protection from an administrator modifying its files. `read(releaseIdentity)`
110
- returns only a completely revalidated stored submission.
111
-
112
- ## Activation registry
113
-
114
- `createFileSystemDeploymentActivationRegistry` adds explicit target activation
115
- without changing accepted release or bundle bytes. The registry requires a
116
- release reader that completely revalidates an accepted release and its closed
117
- bundle before returning it; the filesystem submission store satisfies that
118
- contract directly.
119
-
120
- ```ts
121
- import {
122
- createFileSystemDeploymentActivationRegistry,
123
- createFileSystemDeploymentSubmissionStore,
124
- } from '@hypequery/deployment';
125
-
126
- const directory = '/var/lib/hypequery/deployments';
127
- const releases = createFileSystemDeploymentSubmissionStore({ directory });
128
- const activations = createFileSystemDeploymentActivationRegistry({
129
- directory,
130
- releases,
131
- });
132
-
133
- const target = { project: 'analytics', environment: 'production' };
134
- const current = await activations.current(target);
135
- const result = await activations.activate({
136
- target,
137
- releaseIdentity,
138
- expectedRevision: current?.revision ?? null,
139
- });
140
- ```
141
-
142
- `expectedRevision` is a compare-and-swap precondition. A different active
143
- revision returns `conflict`; requesting the release that is already active
144
- returns `already-active`. Activating an older accepted release performs a
145
- rollback through the same operation and produces a new revision, so stale
146
- pre-rollback callers cannot pass the comparison after an ABA sequence.
147
-
148
- The filesystem implementation stores an immutable, domain-separated activation
149
- record for every transition and derives the current release by verifying the
150
- append-only chain. It has no mutable pointer file or persistent lock to become
151
- stale after a crash. Activation does not load runtime code, route traffic,
152
- perform health checks, or authorize callers; those remain provider concerns.
153
-
154
- ## HTTP control plane
155
-
156
- `createDeploymentControlPlane` combines intake and activation behind closed v1
157
- routes. Activation reads and writes use a separate target-scoped authorizer;
158
- write authorization completes before the small JSON request body is consumed.
159
- The Fetch and Node adapters preserve streaming multipart submission bodies.
160
-
161
- ```ts
162
- import {
163
- createDeploymentControlPlane,
164
- createDeploymentControlPlaneNodeHandler,
165
- } from '@hypequery/deployment';
166
-
167
- const controlPlane = createDeploymentControlPlane({
168
- intake,
169
- activations,
170
- authenticator,
171
- authorizer: {
172
- async authorize({ principal, action, target }) {
173
- return canControlDeployment(principal, action, target);
174
- },
60
+ targets: [{ project: 'analytics', environment: 'production' }],
61
+ intake: {
62
+ authenticator: deploymentAuthenticator,
63
+ authorizer: deploymentAuthorizer,
175
64
  },
65
+ controlPlane: {
66
+ authenticator: operatorAuthenticator,
67
+ authorizer: operatorAuthorizer,
68
+ },
69
+ configureDataPlane,
176
70
  });
177
71
 
178
- const nodeHandler = createDeploymentControlPlaneNodeHandler(controlPlane);
179
- ```
180
-
181
- The control plane exposes release submission, compare-and-swap activation,
182
- current state, and bounded cursor history. It returns stable, bounded JSON error
183
- codes and suppresses internal provider and filesystem details. The HTTP
184
- contract is specified in `specs/deployment/0003-control-plane-http.md`.
185
-
186
- ## Runtime materialization
187
-
188
- `createDeploymentRuntimeMaterializer` converts the current target activation
189
- into a private runtime snapshot. It revalidates the accepted release and closed
190
- bundle, copies and hashes each runtime artifact without following symbolic
191
- links, and confirms the activation revision again before returning.
192
-
193
- ```ts
194
- import { createDeploymentRuntimeMaterializer } from '@hypequery/deployment';
195
-
196
- const materializer = createDeploymentRuntimeMaterializer({
197
- activations,
198
- releases: store,
199
- });
200
-
201
- const snapshot = await materializer.current({
202
- project: 'analytics',
203
- environment: 'production',
204
- });
205
- ```
206
-
207
- Artifact `read()` methods return fresh byte copies, so neither callers nor later
208
- changes to durable storage can alter a materialized snapshot. Runtime imports,
209
- process lifecycle, readiness, and traffic switching remain separate concerns.
210
-
211
- ## Runtime supervision
212
-
213
- `createDeploymentRuntimeSupervisor` starts materialized snapshots through a
214
- runtime factory, checks candidate readiness, confirms activation again, and
215
- atomically changes the generation used for new named-query invocations. Failed
216
- or superseded candidates never displace a healthy generation.
217
-
218
- ```ts
219
- import {
220
- createDeploymentRuntimeSupervisor,
221
- createNodeWorkerDeploymentRuntimeFactory,
222
- } from '@hypequery/deployment';
223
-
224
- const supervisor = createDeploymentRuntimeSupervisor({
225
- materializer,
226
- factory: createNodeWorkerDeploymentRuntimeFactory({
227
- async resolveEnvironment(snapshot, { signal }) {
228
- const connection = await resolveClickHouseConnection(snapshot.target, { signal });
229
- return {
230
- CLICKHOUSE_URL: connection.url,
231
- CLICKHOUSE_DATABASE: connection.database,
232
- CLICKHOUSE_USERNAME: connection.username,
233
- CLICKHOUSE_PASSWORD: connection.password,
234
- };
235
- },
236
- }),
237
- });
238
-
239
- await supervisor.reconcile({ project: 'analytics', environment: 'production' });
240
- const result = await supervisor.invoke({
241
- target: { project: 'analytics', environment: 'production' },
242
- query: 'orders',
243
- argument: { input, ctx: { tenantId } },
244
- });
245
- ```
246
-
247
- Calls already assigned to an old generation may finish after cutover. That
248
- generation receives no new calls and closes when its in-flight work reaches
249
- zero or the drain deadline expires. The reference Node factory loads exact
250
- materialized bytes in worker threads and removes their temporary files on
251
- shutdown. Provider factories can implement Python, process, container, or
252
- remote-sandbox isolation behind the same lifecycle interface.
253
-
254
- `resolveEnvironment(snapshot, { signal })` lets a provider resolve secrets and
255
- configuration for one immutable deployment target before its worker imports any
256
- artifact. When configured, the returned string record replaces the inherited
257
- process environment for that worker, so concurrent targets do not need to
258
- mutate shared `process.env`. Returning an empty record creates a worker with an
259
- empty environment. Omitting the resolver preserves Node's default environment
260
- inheritance for existing single-host integrations.
261
-
262
- Resolvers should return the smallest environment the deployment needs and
263
- honour the startup abort signal during external secret-store calls. Environment
264
- resolution failure prevents the candidate from becoming ready and removes its
265
- temporary artifact bytes.
266
-
267
- The reference worker is a lifecycle boundary, not a hostile-code security
268
- sandbox. Only trusted deployment code should use it directly.
269
-
270
- ## Data-plane execution
271
-
272
- `createDeploymentDataPlane` executes named-query routes from one validated,
273
- immutable deployment contract. It applies bounded input defaults and unknown
274
- property behavior, enforces access and tenant policy, dispatches the declared
275
- implementation kind through an injected adapter, and validates output before
276
- returning it.
277
-
278
- ```ts
279
- import {
280
- createDeploymentDataPlane,
281
- createDeploymentRuntimeSupervisorExecutor,
282
- } from '@hypequery/deployment';
283
-
284
- const executeRuntimeReference = createDeploymentRuntimeSupervisorExecutor({
285
- supervisor,
286
- target,
287
- activationRevision: snapshot.activation.revision,
288
- argument: ({ input, principal, tenant }) => ({ input, principal, tenant }),
289
- });
290
-
291
- const dataPlane = createDeploymentDataPlane({
292
- deployment: snapshot.deployment,
293
- authenticate,
294
- resolveTenant,
295
- executeSemanticPlan,
296
- executeCompiledSql,
297
- executeRuntimeReference,
298
- });
299
-
300
- const result = await dataPlane.execute({
301
- method: 'POST',
302
- path: '/analytics/queries/orders',
303
- credentials,
304
- input: { status: 'paid' },
305
- });
72
+ await service.start();
306
73
  ```
307
74
 
308
- The runtime-supervisor adapter requires the exact activation revision used to
309
- construct the data plane. A later activation therefore cannot accidentally run
310
- against stale route or schema metadata. Argument mapping remains explicit so a
311
- host can preserve the handler contract of its chosen runtime.
75
+ Distributed providers can keep the same interfaces while replacing persistence, runtime isolation, secret resolution, routing, and observability.
312
76
 
313
- Semantic-plan and compiled-SQL adapters own database execution. The core passes
314
- only validated values, the fixed implementation artifact, and closed typed SQL
315
- parameter bindings; it does not select credentials or interpolate SQL.
77
+ ## Trust boundary
316
78
 
317
- ## Data-plane hosting
79
+ The reference Node worker manages lifecycle and immutable generations for trusted deployment code; it is not a hostile-code sandbox. The filesystem store assumes its configured directory is controlled by the operator.
318
80
 
319
- `createDeploymentHost` keeps route/schema execution and supervised runtime
320
- dispatch pinned to the same activation revision. Reconciliation builds a new
321
- data plane from the supervisor's immutable generation view and publishes it
322
- only after confirming that the active generation did not change during
323
- configuration.
81
+ ## Specifications
324
82
 
325
- `createDeploymentDataPlaneFetchHandler` and
326
- `createDeploymentDataPlaneNodeHandler` expose a hosted data plane over HTTP.
327
- They accept either query parameters or one bounded UTF-8 JSON body, reject
328
- duplicate JSON property names, forward cancellation, and return bounded JSON
329
- errors. Public cache metadata is emitted only when execution confirms the
330
- request was public, tenant-independent, and unauthenticated.
83
+ - [Deployment transport](../../specs/deployment/README.md)
84
+ - [Security protocol](../../specs/security-protocol/README.md)
331
85
 
332
- For a single-node service, `createFileSystemDeploymentHost` composes the
333
- filesystem submission store, activation registry, intake, control plane,
334
- runtime materializer, supervisor, and generation-pinned data plane. It
335
- reconciles configured targets at startup and schedules reconciliation after a
336
- durable activation without changing an already-successful activation response
337
- if runtime startup later fails.
338
-
339
- ```ts
340
- import {
341
- createDeploymentDataPlaneNodeHandler,
342
- createFileSystemDeploymentHost,
343
- } from '@hypequery/deployment';
344
-
345
- const service = createFileSystemDeploymentHost({
346
- directory: '/var/lib/hypequery/deployments',
347
- targets: [{ project: 'analytics', environment: 'production' }],
348
- intake: { authenticator: deploymentAuthenticator, authorizer: deploymentAuthorizer },
349
- controlPlane: { authenticator: operatorAuthenticator, authorizer: operatorAuthorizer },
350
- configureDataPlane: () => ({
351
- authenticate: queryAuthenticator,
352
- resolveTenant,
353
- executeSemanticPlan,
354
- executeCompiledSql,
355
- runtimeArgument: ({ input, principal, tenant }) => ({ input, principal, tenant }),
356
- }),
357
- });
358
-
359
- await service.start();
360
- const queryHandler = createDeploymentDataPlaneNodeHandler(
361
- service.host.dataPlane({ project: 'analytics', environment: 'production' }),
362
- );
363
- ```
86
+ Requires Node.js 20 or newer and ESM.
364
87
 
365
- Cloud and other distributed systems can use the same host, supervisor, and HTTP
366
- adapter interfaces while supplying provider-owned persistence, runtime, SQL,
367
- authentication, tenant, routing, and observability implementations. The
368
- filesystem assembly is a reference single-host composition, not a distributed
369
- control plane.
88
+ ## License
370
89
 
371
- The package is ESM-only and requires Node.js 20 or newer.
90
+ Apache-2.0.
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@hypequery/deployment",
3
- "version": "0.7.1",
4
- "description": "Provider-neutral deployment verification and intake for Hypequery",
3
+ "version": "0.7.2",
4
+ "description": "Verified deployment intake, activation, and runtime hosting for Hypequery analytics",
5
+ "keywords": [
6
+ "hypequery",
7
+ "deployment",
8
+ "clickhouse",
9
+ "analytics",
10
+ "verification",
11
+ "runtime"
12
+ ],
5
13
  "license": "Apache-2.0",
6
14
  "type": "module",
7
15
  "main": "dist/index.js",
@@ -18,7 +26,7 @@
18
26
  "README.md"
19
27
  ],
20
28
  "dependencies": {
21
- "@hypequery/protocol": "0.10.1"
29
+ "@hypequery/protocol": "0.10.2"
22
30
  },
23
31
  "devDependencies": {
24
32
  "@types/node": "^22.5.0",
@@ -30,9 +38,13 @@
30
38
  },
31
39
  "repository": {
32
40
  "type": "git",
33
- "url": "https://github.com/hypequery/hypequery.git",
41
+ "url": "git+https://github.com/hypequery/hypequery.git",
34
42
  "directory": "packages/deployment"
35
43
  },
44
+ "homepage": "https://hypequery.com",
45
+ "bugs": {
46
+ "url": "https://github.com/hypequery/hypequery/issues"
47
+ },
36
48
  "publishConfig": {
37
49
  "access": "public"
38
50
  },