@altf4llc/vorpal-sdk 0.1.0-alpha.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.
Files changed (61) hide show
  1. package/dist/api/agent/agent.d.ts +68 -0
  2. package/dist/api/agent/agent.d.ts.map +1 -0
  3. package/dist/api/agent/agent.js +246 -0
  4. package/dist/api/agent/agent.js.map +1 -0
  5. package/dist/api/archive/archive.d.ts +98 -0
  6. package/dist/api/archive/archive.d.ts.map +1 -0
  7. package/dist/api/archive/archive.js +288 -0
  8. package/dist/api/archive/archive.js.map +1 -0
  9. package/dist/api/artifact/artifact.d.ts +169 -0
  10. package/dist/api/artifact/artifact.d.ts.map +1 -0
  11. package/dist/api/artifact/artifact.js +1041 -0
  12. package/dist/api/artifact/artifact.js.map +1 -0
  13. package/dist/api/context/context.d.ts +42 -0
  14. package/dist/api/context/context.d.ts.map +1 -0
  15. package/dist/api/context/context.js +31 -0
  16. package/dist/api/context/context.js.map +1 -0
  17. package/dist/api/worker/worker.d.ts +65 -0
  18. package/dist/api/worker/worker.d.ts.map +1 -0
  19. package/dist/api/worker/worker.js +185 -0
  20. package/dist/api/worker/worker.js.map +1 -0
  21. package/dist/artifact/language/go.d.ts +165 -0
  22. package/dist/artifact/language/go.d.ts.map +1 -0
  23. package/dist/artifact/language/go.js +361 -0
  24. package/dist/artifact/language/go.js.map +1 -0
  25. package/dist/artifact/language/rust.d.ts +136 -0
  26. package/dist/artifact/language/rust.d.ts.map +1 -0
  27. package/dist/artifact/language/rust.js +576 -0
  28. package/dist/artifact/language/rust.js.map +1 -0
  29. package/dist/artifact/language/typescript.d.ts +112 -0
  30. package/dist/artifact/language/typescript.d.ts.map +1 -0
  31. package/dist/artifact/language/typescript.js +232 -0
  32. package/dist/artifact/language/typescript.js.map +1 -0
  33. package/dist/artifact/step.d.ts +28 -0
  34. package/dist/artifact/step.d.ts.map +1 -0
  35. package/dist/artifact/step.js +214 -0
  36. package/dist/artifact/step.js.map +1 -0
  37. package/dist/artifact.d.ts +392 -0
  38. package/dist/artifact.d.ts.map +1 -0
  39. package/dist/artifact.js +938 -0
  40. package/dist/artifact.js.map +1 -0
  41. package/dist/cli.d.ts +42 -0
  42. package/dist/cli.d.ts.map +1 -0
  43. package/dist/cli.js +112 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/context.d.ts +169 -0
  46. package/dist/context.d.ts.map +1 -0
  47. package/dist/context.js +779 -0
  48. package/dist/context.js.map +1 -0
  49. package/dist/index.d.ts +16 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +22 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/system.d.ts +20 -0
  54. package/dist/system.d.ts.map +1 -0
  55. package/dist/system.js +76 -0
  56. package/dist/system.js.map +1 -0
  57. package/dist/vorpal.d.ts +2 -0
  58. package/dist/vorpal.d.ts.map +1 -0
  59. package/dist/vorpal.js +148 -0
  60. package/dist/vorpal.js.map +1 -0
  61. package/package.json +55 -0
@@ -0,0 +1,779 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import * as grpc from "@grpc/grpc-js";
5
+ import { ArtifactServiceClient, } from "./api/artifact/artifact.js";
6
+ import { AgentServiceClient, } from "./api/agent/agent.js";
7
+ import { ContextServiceService, } from "./api/context/context.js";
8
+ import { parseCliArgs } from "./cli.js";
9
+ import { getSystem } from "./system.js";
10
+ // ---------------------------------------------------------------------------
11
+ // TLS credential helper — matches Rust get_client_tls_config()
12
+ // ---------------------------------------------------------------------------
13
+ const VORPAL_ROOT_DIR = "/var/lib/vorpal";
14
+ const VORPAL_CA_PATH = join(VORPAL_ROOT_DIR, "key", "ca.pem");
15
+ function getClientCredentials(uri) {
16
+ if (uri.startsWith("http://") || uri.startsWith("unix://")) {
17
+ return grpc.credentials.createInsecure();
18
+ }
19
+ if (existsSync(VORPAL_CA_PATH)) {
20
+ const caPem = readFileSync(VORPAL_CA_PATH);
21
+ return grpc.credentials.createSsl(caPem);
22
+ }
23
+ // Use system roots (createSsl with no args uses Node's default CA store)
24
+ return grpc.credentials.createSsl();
25
+ }
26
+ /**
27
+ * Converts a URI to a gRPC-js compatible target string.
28
+ *
29
+ * `@grpc/grpc-js` does not understand `https://` or `http://` schemes —
30
+ * it expects `host:port`, `dns:///host:port`, or `unix:///path`.
31
+ * The Rust SDK's `tonic` handles `https://` natively, so the CLI passes
32
+ * registry/agent URLs in that format. We convert them here.
33
+ */
34
+ function toGrpcTarget(uri) {
35
+ if (uri.startsWith("unix://")) {
36
+ return uri;
37
+ }
38
+ if (uri.startsWith("https://")) {
39
+ const host = uri.slice("https://".length).replace(/\/+$/, "");
40
+ return host.includes(":") ? host : `${host}:443`;
41
+ }
42
+ if (uri.startsWith("http://")) {
43
+ const host = uri.slice("http://".length).replace(/\/+$/, "");
44
+ return host.includes(":") ? host : `${host}:80`;
45
+ }
46
+ return uri;
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Credential types and auth header — matches Rust client_auth_header()
50
+ // and Go ClientAuthHeader()
51
+ // ---------------------------------------------------------------------------
52
+ const VORPAL_CREDENTIALS_PATH = join(VORPAL_ROOT_DIR, "key", "credentials.json");
53
+ /**
54
+ * Refreshes an expired access token using the OIDC refresh-token grant.
55
+ *
56
+ * Mirrors `refreshAccessToken` in Go and `refresh_access_token` in Rust:
57
+ * 1. Discover the token endpoint via `<issuer>/.well-known/openid-configuration`
58
+ * 2. POST a `grant_type=refresh_token` form to the token endpoint
59
+ * 3. Return the new access token, expiry, and issued-at timestamp
60
+ */
61
+ async function refreshAccessToken(audience, clientId, issuer, refreshToken) {
62
+ // Discover token endpoint
63
+ const discoveryUrl = `${issuer}/.well-known/openid-configuration`;
64
+ const discoveryResp = await fetch(discoveryUrl);
65
+ if (!discoveryResp.ok) {
66
+ throw new Error(`Failed to fetch OIDC discovery from ${discoveryUrl}: ${discoveryResp.status} ${discoveryResp.statusText}`);
67
+ }
68
+ const discovery = (await discoveryResp.json());
69
+ if (!discovery.token_endpoint) {
70
+ throw new Error("missing token_endpoint in OIDC discovery");
71
+ }
72
+ // Build refresh token request (application/x-www-form-urlencoded)
73
+ const params = new URLSearchParams();
74
+ params.set("grant_type", "refresh_token");
75
+ params.set("client_id", clientId);
76
+ params.set("refresh_token", refreshToken);
77
+ if (audience) {
78
+ params.set("audience", audience);
79
+ }
80
+ const tokenResp = await fetch(discovery.token_endpoint, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
83
+ body: params.toString(),
84
+ });
85
+ if (!tokenResp.ok) {
86
+ throw new Error(`Token refresh failed with status: ${tokenResp.status}`);
87
+ }
88
+ const tokenResult = (await tokenResp.json());
89
+ const expiresIn = tokenResult.expires_in ?? 3600; // default 1 hour
90
+ const issuedAt = Math.floor(Date.now() / 1000);
91
+ return {
92
+ accessToken: tokenResult.access_token,
93
+ expiresIn,
94
+ issuedAt,
95
+ };
96
+ }
97
+ /**
98
+ * Retrieves the authorization header for a given registry.
99
+ *
100
+ * Returns `Bearer <token>` if valid credentials exist, `null` if no
101
+ * credentials file or no mapping for this registry (allowing
102
+ * unauthenticated requests), or throws on unrecoverable errors.
103
+ *
104
+ * Matches Rust `client_auth_header()` and Go `ClientAuthHeader()`.
105
+ */
106
+ async function clientAuthHeader(registry) {
107
+ // Check if credentials file exists
108
+ if (!existsSync(VORPAL_CREDENTIALS_PATH)) {
109
+ return null;
110
+ }
111
+ // Read and parse credentials
112
+ const credentialsData = readFileSync(VORPAL_CREDENTIALS_PATH, "utf-8");
113
+ const credentials = JSON.parse(credentialsData);
114
+ // Lookup registry -> issuer mapping
115
+ const registryIssuer = credentials.registry[registry];
116
+ if (!registryIssuer) {
117
+ // No registry mapping — allow unauthenticated requests
118
+ return null;
119
+ }
120
+ // Lookup issuer credentials
121
+ const issuerCreds = credentials.issuer[registryIssuer];
122
+ if (!issuerCreds) {
123
+ throw new Error(`no credentials for issuer: ${registryIssuer}`);
124
+ }
125
+ // Check if token needs refresh (5-minute buffer, matching Go/Rust)
126
+ const now = Math.floor(Date.now() / 1000);
127
+ const tokenAge = now - issuerCreds.issued_at;
128
+ const needsRefresh = tokenAge + 300 >= issuerCreds.expires_in;
129
+ if (needsRefresh) {
130
+ if (!issuerCreds.refresh_token) {
131
+ throw new Error(`Access token expired and no refresh token available. Please run: vorpal login --issuer ${registryIssuer}`);
132
+ }
133
+ const refreshed = await refreshAccessToken(issuerCreds.audience, issuerCreds.client_id, registryIssuer, issuerCreds.refresh_token);
134
+ // Update credentials in memory
135
+ issuerCreds.access_token = refreshed.accessToken;
136
+ issuerCreds.expires_in = refreshed.expiresIn;
137
+ issuerCreds.issued_at = refreshed.issuedAt;
138
+ // Save updated credentials to disk (matching Go/Rust behavior)
139
+ writeFileSync(VORPAL_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), { mode: 0o600 });
140
+ }
141
+ return `Bearer ${issuerCreds.access_token}`;
142
+ }
143
+ // ---------------------------------------------------------------------------
144
+ // Custom JSON serialization for cross-SDK parity
145
+ // ---------------------------------------------------------------------------
146
+ /**
147
+ * Serializes an Artifact to JSON bytes matching Rust's serde_json::to_vec
148
+ * output for prost-generated structs.
149
+ *
150
+ * Key differences from the generated toJSON:
151
+ * - Field names are snake_case (matching proto field names)
152
+ * - Field order follows proto field number order
153
+ * - ALL fields are always included (even zero-values, empty arrays)
154
+ * - Enums serialize as integers (not strings)
155
+ * - Optional None serializes as null
156
+ *
157
+ * This matches what serde_json produces for prost structs with
158
+ * #[derive(Serialize)] -- all fields present, in declaration order,
159
+ * with no skip_serializing_if attributes.
160
+ */
161
+ export function serializeArtifactStepSecret(secret) {
162
+ return {
163
+ name: secret.name,
164
+ value: secret.value,
165
+ };
166
+ }
167
+ /**
168
+ * Serializes an {@link ArtifactSource} to a plain object matching Rust's serde output.
169
+ * Field order and inclusion rules match the cross-SDK parity requirements.
170
+ */
171
+ export function serializeArtifactSource(source) {
172
+ return {
173
+ digest: source.digest ?? null,
174
+ excludes: source.excludes,
175
+ includes: source.includes,
176
+ name: source.name,
177
+ path: source.path,
178
+ };
179
+ }
180
+ /**
181
+ * Serializes an {@link ArtifactStep} to a plain object matching Rust's serde output.
182
+ * Field order and inclusion rules match the cross-SDK parity requirements.
183
+ */
184
+ export function serializeArtifactStep(step) {
185
+ return {
186
+ entrypoint: step.entrypoint ?? null,
187
+ script: step.script ?? null,
188
+ secrets: step.secrets.map(serializeArtifactStepSecret),
189
+ arguments: step.arguments,
190
+ artifacts: step.artifacts,
191
+ environments: step.environments,
192
+ };
193
+ }
194
+ /**
195
+ * Serializes an {@link Artifact} to a plain object matching Rust's serde output.
196
+ * Field order and inclusion rules match the cross-SDK parity requirements.
197
+ */
198
+ export function serializeArtifact(artifact) {
199
+ return {
200
+ target: artifact.target,
201
+ sources: artifact.sources.map(serializeArtifactSource),
202
+ steps: artifact.steps.map(serializeArtifactStep),
203
+ systems: artifact.systems,
204
+ aliases: artifact.aliases,
205
+ name: artifact.name,
206
+ };
207
+ }
208
+ /**
209
+ * Serializes an Artifact to a JSON string matching Rust serde_json::to_vec.
210
+ * Returns the UTF-8 bytes of the JSON string.
211
+ */
212
+ export function artifactToJsonBytes(artifact) {
213
+ const obj = serializeArtifact(artifact);
214
+ const json = JSON.stringify(obj);
215
+ return Buffer.from(json, "utf-8");
216
+ }
217
+ /**
218
+ * Computes the SHA-256 digest of an artifact using the cross-SDK-compatible
219
+ * JSON serialization. The returned hex string is identical to what the
220
+ * Rust and Go SDKs produce for the same artifact definition.
221
+ */
222
+ export function computeArtifactDigest(artifact) {
223
+ const jsonBytes = artifactToJsonBytes(artifact);
224
+ return createHash("sha256").update(jsonBytes).digest("hex");
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Artifact alias parsing
228
+ // ---------------------------------------------------------------------------
229
+ const DEFAULT_NAMESPACE = "library";
230
+ const DEFAULT_TAG = "latest";
231
+ /**
232
+ * Formats an ArtifactAlias back into its canonical string representation.
233
+ * Omits default namespace ("library") and default tag ("latest").
234
+ */
235
+ export function formatArtifactAlias(alias) {
236
+ const hasNamespace = alias.namespace !== DEFAULT_NAMESPACE;
237
+ const hasTag = alias.tag !== DEFAULT_TAG;
238
+ let result = "";
239
+ if (hasNamespace) {
240
+ result += `${alias.namespace}/`;
241
+ }
242
+ result += alias.name;
243
+ if (hasTag) {
244
+ result += `:${alias.tag}`;
245
+ }
246
+ return result;
247
+ }
248
+ function isValidComponent(s) {
249
+ if (s.length === 0)
250
+ return false;
251
+ for (const c of s) {
252
+ if (!((c >= "a" && c <= "z") ||
253
+ (c >= "A" && c <= "Z") ||
254
+ (c >= "0" && c <= "9") ||
255
+ c === "-" ||
256
+ c === "." ||
257
+ c === "_" ||
258
+ c === "+")) {
259
+ return false;
260
+ }
261
+ }
262
+ return true;
263
+ }
264
+ /**
265
+ * Parses an artifact alias string into its component parts.
266
+ *
267
+ * Format: `[namespace/]name[:tag]`
268
+ *
269
+ * - If no namespace is provided, defaults to `"library"`.
270
+ * - If no tag is provided, defaults to `"latest"`.
271
+ * - Valid characters: alphanumeric, hyphens, dots, underscores, plus signs.
272
+ * - Maximum length: 255 characters.
273
+ *
274
+ * @param alias - The alias string to parse
275
+ * @returns Parsed {@link ArtifactAlias} with defaults applied
276
+ * @throws If the alias is empty, too long, or contains invalid characters
277
+ */
278
+ export function parseArtifactAlias(alias) {
279
+ if (alias.length === 0) {
280
+ throw new Error("alias cannot be empty");
281
+ }
282
+ if (alias.length > 255) {
283
+ throw new Error("alias too long (max 255 characters)");
284
+ }
285
+ // Step 1: Extract tag (split on rightmost ':')
286
+ let base;
287
+ let tag;
288
+ const lastColon = alias.lastIndexOf(":");
289
+ if (lastColon !== -1) {
290
+ const tagPart = alias.substring(lastColon + 1);
291
+ if (tagPart === "") {
292
+ throw new Error("tag cannot be empty");
293
+ }
294
+ tag = tagPart;
295
+ base = alias.substring(0, lastColon);
296
+ }
297
+ else {
298
+ tag = "";
299
+ base = alias;
300
+ }
301
+ // Step 2: Extract namespace/name
302
+ let namespace;
303
+ let name;
304
+ const slashIdx = base.indexOf("/");
305
+ if (slashIdx === -1) {
306
+ namespace = "";
307
+ name = base;
308
+ }
309
+ else {
310
+ namespace = base.substring(0, slashIdx);
311
+ const rest = base.substring(slashIdx + 1);
312
+ if (namespace === "") {
313
+ throw new Error("namespace cannot be empty");
314
+ }
315
+ if (rest.includes("/")) {
316
+ throw new Error("invalid format: too many path separators");
317
+ }
318
+ name = rest;
319
+ }
320
+ if (name === "") {
321
+ throw new Error("name is required");
322
+ }
323
+ // Step 3: Validate component characters
324
+ if (!isValidComponent(name)) {
325
+ throw new Error("name contains invalid characters (allowed: alphanumeric, hyphens, dots, underscores, plus signs)");
326
+ }
327
+ if (namespace !== "" && !isValidComponent(namespace)) {
328
+ throw new Error("namespace contains invalid characters (allowed: alphanumeric, hyphens, dots, underscores, plus signs)");
329
+ }
330
+ if (tag !== "" && !isValidComponent(tag)) {
331
+ throw new Error("tag contains invalid characters (allowed: alphanumeric, hyphens, dots, underscores, plus signs)");
332
+ }
333
+ // Step 4: Apply defaults
334
+ if (tag === "") {
335
+ tag = DEFAULT_TAG;
336
+ }
337
+ if (namespace === "") {
338
+ namespace = DEFAULT_NAMESPACE;
339
+ }
340
+ return { name, namespace, tag };
341
+ }
342
+ // ---------------------------------------------------------------------------
343
+ // ConfigContext
344
+ // ---------------------------------------------------------------------------
345
+ export class ConfigContext {
346
+ _artifact;
347
+ _artifactContext;
348
+ _artifactNamespace;
349
+ _artifactSystem;
350
+ _artifactUnlock;
351
+ _clientAgent;
352
+ _clientArtifact;
353
+ _port;
354
+ _registry;
355
+ _store;
356
+ constructor(artifact, artifactContext, artifactNamespace, artifactSystem, artifactUnlock, clientAgent, clientArtifact, port, registry, store) {
357
+ this._artifact = artifact;
358
+ this._artifactContext = artifactContext;
359
+ this._artifactNamespace = artifactNamespace;
360
+ this._artifactSystem = artifactSystem;
361
+ this._artifactUnlock = artifactUnlock;
362
+ this._clientAgent = clientAgent;
363
+ this._clientArtifact = clientArtifact;
364
+ this._port = port;
365
+ this._registry = registry;
366
+ this._store = store;
367
+ }
368
+ /**
369
+ * Creates a ConfigContext by parsing CLI arguments and connecting to
370
+ * gRPC services. Matches Rust get_context() and Go GetContext().
371
+ */
372
+ static create(argv) {
373
+ let args;
374
+ try {
375
+ args = parseCliArgs(argv);
376
+ }
377
+ catch (err) {
378
+ const msg = err instanceof Error ? err.message : String(err);
379
+ throw new Error(`Failed to parse CLI arguments: ${msg}\n\n` +
380
+ ` This usually means the compiled TypeScript config was invoked\n` +
381
+ ` with incorrect or missing arguments. The Vorpal CLI should\n` +
382
+ ` supply these automatically during 'vorpal build'.\n\n` +
383
+ ` If you are running the config binary manually, the required\n` +
384
+ ` arguments are:\n` +
385
+ ` start --agent <URL> --artifact <NAME> --artifact-context <PATH>\n` +
386
+ ` --artifact-namespace <NS> --artifact-system <SYSTEM>\n` +
387
+ ` --port <PORT> --registry <URL>\n`);
388
+ }
389
+ let artifactSystem;
390
+ try {
391
+ artifactSystem = getSystem(args.artifactSystem);
392
+ }
393
+ catch (_err) {
394
+ throw new Error(`Unsupported artifact system: '${args.artifactSystem}'\n\n` +
395
+ ` Supported systems are:\n` +
396
+ ` - aarch64-darwin (Apple Silicon macOS)\n` +
397
+ ` - aarch64-linux (ARM64 Linux)\n` +
398
+ ` - x86_64-darwin (Intel macOS)\n` +
399
+ ` - x86_64-linux (Intel/AMD Linux)\n`);
400
+ }
401
+ // Parse variables
402
+ const variables = new Map();
403
+ for (const v of args.artifactVariable) {
404
+ const eqIdx = v.indexOf("=");
405
+ if (eqIdx !== -1) {
406
+ const name = v.substring(0, eqIdx);
407
+ const value = v.substring(eqIdx + 1);
408
+ variables.set(name, value);
409
+ }
410
+ }
411
+ // Create gRPC clients (TLS based on URI scheme, matching Rust SDK)
412
+ let clientAgent;
413
+ let clientArtifact;
414
+ try {
415
+ const agentTarget = toGrpcTarget(args.agent);
416
+ const agentCredentials = getClientCredentials(args.agent);
417
+ clientAgent = new AgentServiceClient(agentTarget, agentCredentials);
418
+ }
419
+ catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ throw new Error(`Failed to connect to agent service at '${args.agent}': ${msg}\n\n` +
422
+ ` Make sure the Vorpal agent is running. You can start it with:\n` +
423
+ ` vorpal system services start\n\n` +
424
+ ` If using a custom agent address, verify the --agent URL is correct.\n`);
425
+ }
426
+ try {
427
+ const registryTarget = toGrpcTarget(args.registry);
428
+ const registryCredentials = getClientCredentials(args.registry);
429
+ clientArtifact = new ArtifactServiceClient(registryTarget, registryCredentials);
430
+ }
431
+ catch (err) {
432
+ const msg = err instanceof Error ? err.message : String(err);
433
+ throw new Error(`Failed to connect to registry service at '${args.registry}': ${msg}\n\n` +
434
+ ` Make sure the Vorpal registry is running. You can start it with:\n` +
435
+ ` vorpal system services start\n\n` +
436
+ ` If using a custom registry address, verify the --registry URL is correct.\n`);
437
+ }
438
+ return new ConfigContext(args.artifact, args.artifactContext, args.artifactNamespace, artifactSystem, args.artifactUnlock, clientAgent, clientArtifact, args.port, args.registry, {
439
+ artifact: new Map(),
440
+ artifactInputCache: new Map(),
441
+ variable: variables,
442
+ });
443
+ }
444
+ /**
445
+ * Adds an artifact to the context, computing its digest and sending it
446
+ * to the agent service for preparation.
447
+ *
448
+ * The SHA-256 digest is computed from the JSON serialization of the
449
+ * artifact, using the custom serializer that matches Rust's
450
+ * serde_json::to_vec output.
451
+ */
452
+ async addArtifact(artifact) {
453
+ if (artifact.name === "") {
454
+ throw new Error("name cannot be empty");
455
+ }
456
+ if (artifact.steps.length === 0) {
457
+ throw new Error("steps cannot be empty");
458
+ }
459
+ if (artifact.systems.length === 0) {
460
+ throw new Error("systems cannot be empty");
461
+ }
462
+ // Validate target is in systems list
463
+ if (!artifact.systems.includes(artifact.target)) {
464
+ throw new Error(`artifact '${artifact.name}' does not support system '${artifact.target}' (supported: ${artifact.systems.join(", ")})`);
465
+ }
466
+ // Serialize and compute digest -- CRITICAL PATH for cross-SDK parity
467
+ const artifactJson = artifactToJsonBytes(artifact);
468
+ const artifactDigest = createHash("sha256")
469
+ .update(artifactJson)
470
+ .digest("hex");
471
+ if (this._store.artifact.has(artifactDigest)) {
472
+ return artifactDigest;
473
+ }
474
+ const cachedOutputDigest = this._store.artifactInputCache.get(artifactDigest);
475
+ if (cachedOutputDigest && this._store.artifact.has(cachedOutputDigest)) {
476
+ return cachedOutputDigest;
477
+ }
478
+ const inputDigest = artifactDigest;
479
+ // Send to agent for preparation
480
+ const request = {
481
+ artifact_unlock: this._artifactUnlock,
482
+ artifact_context: this._artifactContext,
483
+ artifact_namespace: this._artifactNamespace,
484
+ registry: this._registry,
485
+ artifact: artifact,
486
+ };
487
+ // Add authorization metadata if credentials exist (matches Go/Rust SDKs)
488
+ const metadata = new grpc.Metadata();
489
+ const bearerToken = await clientAuthHeader(this._registry);
490
+ if (bearerToken) {
491
+ metadata.set("authorization", bearerToken);
492
+ }
493
+ const stream = this._clientAgent.prepareArtifact(request, metadata);
494
+ let responseArtifact;
495
+ let responseArtifactDigest;
496
+ await new Promise((resolve, reject) => {
497
+ stream.on("data", (response) => {
498
+ if (response.artifact_output) {
499
+ console.log(`${artifact.name} |> ${response.artifact_output}`);
500
+ }
501
+ if (response.artifact) {
502
+ responseArtifact = response.artifact;
503
+ }
504
+ if (response.artifact_digest) {
505
+ responseArtifactDigest = response.artifact_digest;
506
+ }
507
+ });
508
+ stream.on("end", () => resolve());
509
+ stream.on("error", (err) => {
510
+ if (err.code === grpc.status.NOT_FOUND) {
511
+ reject(new Error(`Artifact '${artifact.name}' not found in agent service.\n\n` +
512
+ ` The agent does not have this artifact registered.\n` +
513
+ ` This can happen if the agent was restarted or the artifact\n` +
514
+ ` has not been built yet.\n`));
515
+ }
516
+ else if (err.code === grpc.status.UNAVAILABLE) {
517
+ reject(new Error(`Agent service is unavailable (connection refused or dropped).\n\n` +
518
+ ` Could not reach the agent at the configured address.\n\n` +
519
+ ` To fix this:\n` +
520
+ ` 1. Make sure the Vorpal agent is running:\n` +
521
+ ` vorpal system services start\n` +
522
+ ` 2. Check that the agent address is correct in your config.\n`));
523
+ }
524
+ else if (err.code === grpc.status.DEADLINE_EXCEEDED) {
525
+ reject(new Error(`Agent service request timed out for artifact '${artifact.name}'.\n\n` +
526
+ ` The agent took too long to respond. This may indicate:\n` +
527
+ ` - The agent is overloaded or under heavy build load\n` +
528
+ ` - Network connectivity issues between client and agent\n\n` +
529
+ ` Try again, or check agent logs for more details.\n`));
530
+ }
531
+ else {
532
+ reject(new Error(`gRPC error from agent service (code=${err.code}): ${err.message}\n\n` +
533
+ ` An unexpected error occurred while communicating with the agent.\n` +
534
+ ` Check the agent logs for more details.\n`));
535
+ }
536
+ });
537
+ });
538
+ if (!responseArtifact) {
539
+ throw new Error("artifact not returned from agent service");
540
+ }
541
+ if (!responseArtifactDigest) {
542
+ throw new Error("artifact digest not returned from agent service");
543
+ }
544
+ this._store.artifact.set(responseArtifactDigest, responseArtifact);
545
+ this._store.artifactInputCache.set(inputDigest, responseArtifactDigest);
546
+ return responseArtifactDigest;
547
+ }
548
+ /**
549
+ * Fetches an artifact by digest from the artifact service (registry).
550
+ * Recursively fetches all step dependencies.
551
+ */
552
+ async fetchArtifact(digest) {
553
+ return this.fetchArtifactInNamespace(digest, this._artifactNamespace);
554
+ }
555
+ /**
556
+ * Fetches an artifact by digest in a specific namespace.
557
+ * Recursively fetches all step dependencies using the same namespace.
558
+ * This mirrors the Rust SDK's `fetch_artifact_in_namespace`.
559
+ */
560
+ async fetchArtifactInNamespace(digest, namespace) {
561
+ if (this._store.artifact.has(digest)) {
562
+ return digest;
563
+ }
564
+ const request = {
565
+ digest: digest,
566
+ namespace: namespace,
567
+ };
568
+ // Add authorization metadata if credentials exist (matches Go/Rust SDKs)
569
+ const metadata = new grpc.Metadata();
570
+ const bearerToken = await clientAuthHeader(this._registry);
571
+ if (bearerToken) {
572
+ metadata.set("authorization", bearerToken);
573
+ }
574
+ const artifact = await new Promise((resolve, reject) => {
575
+ this._clientArtifact.getArtifact(request, metadata, (err, response) => {
576
+ if (err) {
577
+ const svcErr = err;
578
+ if (svcErr.code === grpc.status.NOT_FOUND) {
579
+ reject(new Error(`Artifact not found in registry (digest: ${digest}).\n\n` +
580
+ ` The registry does not have an artifact with this digest.\n` +
581
+ ` This can happen if the artifact was never pushed or has been pruned.\n`));
582
+ }
583
+ else if (svcErr.code === grpc.status.UNAVAILABLE) {
584
+ reject(new Error(`Registry service is unavailable.\n\n` +
585
+ ` Could not reach the registry at '${this._registry}'.\n\n` +
586
+ ` To fix this:\n` +
587
+ ` 1. Make sure the Vorpal registry is running:\n` +
588
+ ` vorpal system services start\n` +
589
+ ` 2. Check that the registry address is correct.\n`));
590
+ }
591
+ else {
592
+ reject(new Error(`Registry service error (code=${svcErr.code}): ${err.message}\n\n` +
593
+ ` An unexpected error occurred while fetching artifact '${digest}'.\n` +
594
+ ` Check the registry logs for more details.\n`));
595
+ }
596
+ }
597
+ else {
598
+ resolve(response);
599
+ }
600
+ });
601
+ });
602
+ this._store.artifact.set(digest, artifact);
603
+ for (const step of artifact.steps) {
604
+ for (const dep of step.artifacts) {
605
+ await this.fetchArtifactInNamespace(dep, namespace);
606
+ }
607
+ }
608
+ return digest;
609
+ }
610
+ /**
611
+ * Fetches an artifact by alias from the artifact service (registry).
612
+ * Uses the Go SDK approach: FetchArtifactAlias.
613
+ */
614
+ async fetchArtifactAlias(alias) {
615
+ const parsed = parseArtifactAlias(alias);
616
+ const request = {
617
+ system: this._artifactSystem,
618
+ name: parsed.name,
619
+ namespace: parsed.namespace,
620
+ tag: parsed.tag,
621
+ };
622
+ // Add authorization metadata if credentials exist (matches Go/Rust SDKs)
623
+ const metadata = new grpc.Metadata();
624
+ const bearerToken = await clientAuthHeader(this._registry);
625
+ if (bearerToken) {
626
+ metadata.set("authorization", bearerToken);
627
+ }
628
+ const response = await new Promise((resolve, reject) => {
629
+ this._clientArtifact.getArtifactAlias(request, metadata, (err, resp) => {
630
+ if (err) {
631
+ const svcErr = err;
632
+ if (svcErr.code === grpc.status.NOT_FOUND) {
633
+ reject(new Error(`Artifact alias '${alias}' not found in registry.\n\n` +
634
+ ` No artifact matches namespace='${parsed.namespace}', ` +
635
+ `name='${parsed.name}', tag='${parsed.tag}'.\n\n` +
636
+ ` Make sure the artifact has been built and published,\n` +
637
+ ` and that the alias is spelled correctly.\n`));
638
+ }
639
+ else if (svcErr.code === grpc.status.UNAVAILABLE) {
640
+ reject(new Error(`Registry service is unavailable.\n\n` +
641
+ ` Could not reach the registry at '${this._registry}'.\n\n` +
642
+ ` To fix this:\n` +
643
+ ` 1. Make sure the Vorpal registry is running:\n` +
644
+ ` vorpal system services start\n` +
645
+ ` 2. Check that the registry address is correct.\n`));
646
+ }
647
+ else {
648
+ reject(new Error(`Registry error fetching alias '${alias}' (code=${svcErr.code}): ${err.message}\n\n` +
649
+ ` Check the registry logs for more details.\n`));
650
+ }
651
+ }
652
+ else {
653
+ resolve(resp);
654
+ }
655
+ });
656
+ });
657
+ const artifactDigest = response.digest;
658
+ if (!artifactDigest) {
659
+ throw new Error(`Registry returned empty digest for alias: ${alias}`);
660
+ }
661
+ if (this._store.artifact.has(artifactDigest)) {
662
+ return artifactDigest;
663
+ }
664
+ await this.fetchArtifactInNamespace(artifactDigest, parsed.namespace);
665
+ return artifactDigest;
666
+ }
667
+ /**
668
+ * Returns a shallow copy of the artifact store (digest -> Artifact).
669
+ * Useful for inspecting all artifacts registered during this config run.
670
+ */
671
+ getArtifactStore() {
672
+ return new Map(this._store.artifact);
673
+ }
674
+ /**
675
+ * Looks up a previously registered artifact by its digest.
676
+ *
677
+ * @param digest - The hex-encoded SHA-256 digest
678
+ * @returns The artifact, or `undefined` if not found
679
+ */
680
+ getArtifact(digest) {
681
+ return this._store.artifact.get(digest);
682
+ }
683
+ /** Returns the filesystem path to the artifact context directory. */
684
+ getArtifactContextPath() {
685
+ return this._artifactContext;
686
+ }
687
+ /** Returns the name of the top-level artifact being built. */
688
+ getArtifactName() {
689
+ return this._artifact;
690
+ }
691
+ /** Returns the namespace used for artifact registration and lookup. */
692
+ getArtifactNamespace() {
693
+ return this._artifactNamespace;
694
+ }
695
+ /**
696
+ * Returns the target {@link ArtifactSystem} for this build
697
+ * (e.g., `ArtifactSystem.AARCH64_DARWIN`).
698
+ */
699
+ getSystem() {
700
+ return this._artifactSystem;
701
+ }
702
+ /**
703
+ * Looks up a build variable by name. Variables are passed via
704
+ * `--artifact-variable KEY=VALUE` on the CLI.
705
+ *
706
+ * @param name - Variable name
707
+ * @returns The variable value, or `undefined` if not set
708
+ */
709
+ getVariable(name) {
710
+ return this._store.variable.get(name);
711
+ }
712
+ /**
713
+ * Starts the ContextService gRPC server.
714
+ * Matches Rust ConfigContext::run() and Go ConfigContext.Run().
715
+ *
716
+ * Prints "context service: [::]:PORT" to stdout for CLI detection.
717
+ */
718
+ async run() {
719
+ const server = new grpc.Server();
720
+ const store = this._store;
721
+ server.addService(ContextServiceService, {
722
+ getArtifact: (call, callback) => {
723
+ const request = call.request;
724
+ if (!request.digest || request.digest === "") {
725
+ callback({
726
+ code: grpc.status.INVALID_ARGUMENT,
727
+ message: "'digest' is required",
728
+ });
729
+ return;
730
+ }
731
+ const artifact = store.artifact.get(request.digest);
732
+ if (!artifact) {
733
+ callback({
734
+ code: grpc.status.NOT_FOUND,
735
+ message: "artifact not found",
736
+ });
737
+ return;
738
+ }
739
+ callback(null, artifact);
740
+ },
741
+ getArtifacts: (_call, callback) => {
742
+ const digests = Array.from(store.artifact.keys()).sort();
743
+ callback(null, { digests });
744
+ },
745
+ });
746
+ const addr = `[::]:${this._port}`;
747
+ await new Promise((resolve, reject) => {
748
+ server.bindAsync(addr, grpc.ServerCredentials.createInsecure(), (err) => {
749
+ if (err) {
750
+ reject(new Error(`Failed to bind context service to ${addr}: ${err.message}\n\n` +
751
+ ` The TypeScript config's gRPC context server could not start.\n` +
752
+ ` This usually means the port is already in use by another process.\n\n` +
753
+ ` To fix this:\n` +
754
+ ` 1. Check if another Vorpal config process is still running\n` +
755
+ ` 2. Try running 'vorpal build' again (a new port will be selected)\n`));
756
+ return;
757
+ }
758
+ resolve();
759
+ });
760
+ });
761
+ console.log(`context service: ${addr}`);
762
+ // Keep the server running until SIGINT/SIGTERM
763
+ await new Promise((resolve) => {
764
+ const shutdown = () => {
765
+ server.tryShutdown((err) => {
766
+ if (err) {
767
+ console.error(`Warning: context service shutdown error: ${err.message}`);
768
+ console.error(` Forcing shutdown. This is usually harmless.`);
769
+ server.forceShutdown();
770
+ }
771
+ resolve();
772
+ });
773
+ };
774
+ process.on("SIGINT", shutdown);
775
+ process.on("SIGTERM", shutdown);
776
+ });
777
+ }
778
+ }
779
+ //# sourceMappingURL=context.js.map