@camstack/server 1.2.501 → 1.2.502

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.
@@ -44,6 +44,8 @@ const moleculer_1 = require("moleculer");
44
44
  const agent_deploy_swap_js_1 = require("./agent-deploy-swap.js");
45
45
  const agent_undeploy_js_1 = require("./agent-undeploy.js");
46
46
  const apply_model_distribution_js_1 = require("./apply-model-distribution.js");
47
+ const apply_stage_dev_uploads_js_1 = require("./apply-stage-dev-uploads.js");
48
+ const dev_uploads_bundle_js_1 = require("../api/dev-uploads-bundle.js");
47
49
  const fetch_bundle_from_hub_js_1 = require("./fetch-bundle-from-hub.js");
48
50
  /**
49
51
  * Console-backed scoped logger for the deploy swap (the only consumer in this
@@ -459,6 +461,23 @@ function createAgentService(deps) {
459
461
  return resolveDeployAction(seam)({ addonId, source, bundle });
460
462
  },
461
463
  },
464
+ /**
465
+ * Stage a hub-packed `dev-uploads/<version>/` set under this node's
466
+ * `server-root/dev-uploads/` so a subsequent `applyServerUpdate` for a
467
+ * `-dev.<epoch>` version can promote locally (Path B fan-out). Does not
468
+ * restart or promote — only writes the upload layout.
469
+ */
470
+ stageDevUploads: {
471
+ handler: async (ctx) => {
472
+ const { params } = ctx;
473
+ const seam = {
474
+ dataDir: deps.dataDir,
475
+ fetchBundle: (s) => (0, fetch_bundle_from_hub_js_1.fetchBundleFromHub)(s),
476
+ unpack: dev_uploads_bundle_js_1.unpackDevUploadVersionBundle,
477
+ };
478
+ return (0, apply_stage_dev_uploads_js_1.applyStageDevUploads)(seam, params);
479
+ },
480
+ },
462
481
  /**
463
482
  * Pull a staged model tarball from the hub and untar it into this node's
464
483
  * `<dataDir>/models` (Model Studio P2). Reuses the agent-pull machinery:
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyStageDevUploads = applyStageDevUploads;
4
+ /**
5
+ * Pure seam for `$agent.stageDevUploads` — fetch a hub-staged pack of
6
+ * `dev-uploads/<version>/` and write it under this node's server-root.
7
+ */
8
+ const node_crypto_1 = require("node:crypto");
9
+ const index_js_1 = require("../server-root/index.js");
10
+ async function applyStageDevUploads(seam, params) {
11
+ const { version, source } = params;
12
+ if (!(0, index_js_1.isDevChannelVersion)(version)) {
13
+ throw new Error(`$agent.stageDevUploads: version "${version}" is not dev-channel shaped`);
14
+ }
15
+ const bundle = await seam.fetchBundle(source);
16
+ const appliedSha256 = (0, node_crypto_1.createHash)('sha256').update(bundle).digest('hex');
17
+ const unpacked = await seam.unpack({
18
+ serverRoot: (0, index_js_1.serverRootDir)(seam.dataDir),
19
+ version,
20
+ bundle,
21
+ });
22
+ return {
23
+ success: true,
24
+ version,
25
+ path: unpacked.versionDir,
26
+ packageCount: unpacked.packageCount,
27
+ appliedSha256,
28
+ };
29
+ }
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isSafeDevUploadVersion = isSafeDevUploadVersion;
37
+ exports.packDevUploadVersionDir = packDevUploadVersionDir;
38
+ exports.unpackDevUploadVersionBundle = unpackDevUploadVersionBundle;
39
+ /**
40
+ * Pack / unpack a hub `server-root/dev-uploads/<version>/` directory for
41
+ * agent fan-out (`POST /api/server-upload/propagate` → `$agent.stageDevUploads`).
42
+ *
43
+ * One gzipped tar carries every tarball + `manifest.json`. Fail-closed on
44
+ * path traversal and on a layout that would not satisfy RootUpdateService.
45
+ */
46
+ const fs = __importStar(require("node:fs"));
47
+ const os = __importStar(require("node:os"));
48
+ const path = __importStar(require("node:path"));
49
+ const tar = __importStar(require("tar"));
50
+ const index_js_1 = require("../server-root/index.js");
51
+ /** Version chars safe as a single path segment (matches server-upload). */
52
+ const SAFE_VERSION_RE = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
53
+ function isSafeDevUploadVersion(version) {
54
+ return SAFE_VERSION_RE.test(version) && (0, index_js_1.isDevChannelVersion)(version);
55
+ }
56
+ function hasPathTraversal(name) {
57
+ return name.includes('/') || name.includes('\\') || name.includes('..');
58
+ }
59
+ /**
60
+ * Pack every file in `versionDir` (basename entries only) into a gzipped tar.
61
+ * Refuses if the manifest is missing/invalid or lists a missing tarball.
62
+ */
63
+ async function packDevUploadVersionDir(versionDir) {
64
+ const manifest = (0, index_js_1.readDevUploadManifest)(versionDir);
65
+ if (manifest === null) {
66
+ throw new Error(`dev-uploads manifest missing or invalid in ${versionDir}`);
67
+ }
68
+ for (const filename of Object.values(manifest.packages)) {
69
+ if (hasPathTraversal(filename)) {
70
+ throw new Error(`dev-uploads package filename refuses path traversal: ${filename}`);
71
+ }
72
+ const abs = path.join(versionDir, filename);
73
+ if (!fs.existsSync(abs)) {
74
+ throw new Error(`dev-uploads tarball listed in manifest is missing: ${filename}`);
75
+ }
76
+ }
77
+ if (!fs.existsSync(path.join(versionDir, index_js_1.DEV_UPLOAD_MANIFEST_FILE))) {
78
+ throw new Error(`dev-uploads manifest file missing in ${versionDir}`);
79
+ }
80
+ const entries = fs.readdirSync(versionDir).filter((name) => {
81
+ const st = fs.statSync(path.join(versionDir, name));
82
+ return st.isFile() && !hasPathTraversal(name);
83
+ });
84
+ if (entries.length === 0) {
85
+ throw new Error(`dev-uploads version dir is empty: ${versionDir}`);
86
+ }
87
+ const chunks = [];
88
+ await new Promise((resolve, reject) => {
89
+ const stream = tar.c({ gzip: true, cwd: versionDir, portable: true }, entries);
90
+ stream.on('data', (chunk) => {
91
+ chunks.push(chunk);
92
+ });
93
+ stream.on('error', reject);
94
+ stream.on('end', () => resolve());
95
+ });
96
+ return Buffer.concat(chunks);
97
+ }
98
+ /**
99
+ * Extract a packed version dir into `serverRoot/dev-uploads/<version>/`
100
+ * atomically (stage sibling + rename). Validates version shape, entry names,
101
+ * and that the extracted layout matches the manifest.
102
+ */
103
+ async function unpackDevUploadVersionBundle(args) {
104
+ const { serverRoot, version, bundle } = args;
105
+ if (!isSafeDevUploadVersion(version)) {
106
+ throw new Error(`Refused: version "${version}" is not a safe dev-channel version for staging`);
107
+ }
108
+ const uploadsRoot = path.join(serverRoot, 'dev-uploads');
109
+ fs.mkdirSync(uploadsRoot, { recursive: true });
110
+ const finalDir = path.join(uploadsRoot, version);
111
+ const stagingDir = path.join(uploadsRoot, `.staging-${version}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
112
+ fs.mkdirSync(stagingDir, { recursive: true });
113
+ const tmpTgz = path.join(os.tmpdir(), `camstack-dev-uploads-${version.replace(/[^\w.-]/g, '_')}-${Date.now()}.tgz`);
114
+ try {
115
+ fs.writeFileSync(tmpTgz, bundle);
116
+ let refusedEntry = null;
117
+ await tar.x({
118
+ file: tmpTgz,
119
+ cwd: stagingDir,
120
+ gzip: true,
121
+ // Basename-only layout. Skipping the entry (filter false) then failing
122
+ // after extract avoids an unhandled throw from tar's async parser.
123
+ filter: (entryPath) => {
124
+ const name = entryPath.replace(/^\.\//, '');
125
+ if (hasPathTraversal(name) || path.isAbsolute(name) || name.includes('/')) {
126
+ refusedEntry = entryPath;
127
+ return false;
128
+ }
129
+ return true;
130
+ },
131
+ });
132
+ if (refusedEntry !== null) {
133
+ throw new Error(`Refused: archive entry is not a basename: ${refusedEntry}`);
134
+ }
135
+ const manifest = (0, index_js_1.readDevUploadManifest)(stagingDir);
136
+ if (manifest === null) {
137
+ throw new Error('extracted archive has no valid manifest.json');
138
+ }
139
+ if (manifest.version !== version) {
140
+ throw new Error(`extracted manifest version mismatch: expected ${version}, got ${manifest.version}`);
141
+ }
142
+ for (const filename of Object.values(manifest.packages)) {
143
+ if (hasPathTraversal(filename)) {
144
+ throw new Error(`manifest package filename refuses path traversal: ${filename}`);
145
+ }
146
+ if (!fs.existsSync(path.join(stagingDir, filename))) {
147
+ throw new Error(`extracted archive missing package tarball: ${filename}`);
148
+ }
149
+ }
150
+ // Replace any previous incomplete/stale version dir.
151
+ fs.rmSync(finalDir, { recursive: true, force: true });
152
+ fs.renameSync(stagingDir, finalDir);
153
+ return {
154
+ versionDir: finalDir,
155
+ packageCount: Object.keys(manifest.packages).length,
156
+ };
157
+ }
158
+ catch (err) {
159
+ fs.rmSync(stagingDir, { recursive: true, force: true });
160
+ throw err;
161
+ }
162
+ finally {
163
+ try {
164
+ fs.unlinkSync(tmpTgz);
165
+ }
166
+ catch {
167
+ /* best-effort */
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerServerUploadPropagateRoute = registerServerUploadPropagateRoute;
37
+ /**
38
+ * POST /api/server-upload/propagate — deliver an already-uploaded
39
+ * `dev-uploads/<version>/` set from the hub onto one agent, then leave
40
+ * `applyServerUpdate` to the caller (CLI).
41
+ *
42
+ * Mirrors addon fan-out: pack the version dir → stage one-shot hub-http
43
+ * ticket → `$agent.stageDevUploads`. Does NOT promote the closure.
44
+ */
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
47
+ const agent_addon_delivery_js_1 = require("./agent-addon-delivery.js");
48
+ const addon_upload_js_1 = require("./addon-upload.js");
49
+ const dev_uploads_bundle_js_1 = require("./dev-uploads-bundle.js");
50
+ const upload_auth_js_1 = require("./upload-auth.js");
51
+ const index_js_1 = require("../server-root/index.js");
52
+ function isStageResponse(value) {
53
+ if (value === null || typeof value !== 'object')
54
+ return false;
55
+ return typeof value.success === 'boolean';
56
+ }
57
+ function readBody(value) {
58
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
59
+ return { error: 'Expected JSON body { version, nodeId }' };
60
+ }
61
+ const body = value;
62
+ if (typeof body.version !== 'string' || body.version.length === 0) {
63
+ return { error: 'Missing "version" field' };
64
+ }
65
+ if (typeof body.nodeId !== 'string' || body.nodeId.length === 0) {
66
+ return { error: 'Missing "nodeId" field' };
67
+ }
68
+ if (body.nodeId === 'hub' || body.nodeId.includes('/')) {
69
+ return {
70
+ error: 'nodeId must be a top-level agent id (not "hub" and not a child runner id)',
71
+ };
72
+ }
73
+ if (!(0, dev_uploads_bundle_js_1.isSafeDevUploadVersion)(body.version)) {
74
+ return {
75
+ error: `Refused: version "${body.version}" is not a safe dev-channel version ` +
76
+ '(expected `<base>-dev.<epochSeconds>`)',
77
+ };
78
+ }
79
+ return { version: body.version, nodeId: body.nodeId };
80
+ }
81
+ function registerServerUploadPropagateRoute(fastify, options) {
82
+ const { authService, addonRegistry, logger, broker, resolveClusterBaseUrl } = options;
83
+ fastify.post('/api/server-upload/propagate', async (request, reply) => {
84
+ const auth = await (0, upload_auth_js_1.authorizeUploadRequest)({
85
+ authHeader: request.headers.authorization,
86
+ authService,
87
+ addonRegistry,
88
+ });
89
+ if (!auth.ok) {
90
+ return reply.status(auth.status).send({ error: auth.error });
91
+ }
92
+ const parsed = readBody(request.body);
93
+ if ('error' in parsed) {
94
+ return reply.status(400).send({ error: parsed.error });
95
+ }
96
+ const { version, nodeId } = parsed;
97
+ const dataDir = path.resolve(options.dataDir ?? process.env['CAMSTACK_DATA'] ?? 'camstack-data');
98
+ const versionDir = (0, index_js_1.devUploadVersionDir)((0, index_js_1.serverRootDir)(dataDir), version);
99
+ if (!fs.existsSync(versionDir)) {
100
+ return reply.status(404).send({
101
+ error: `No hub-side upload for ${version} at ${versionDir}. ` +
102
+ 'Upload first with `camstack deploy-server` (POST /api/server-upload).',
103
+ });
104
+ }
105
+ const manifest = (0, index_js_1.readDevUploadManifest)(versionDir);
106
+ if (manifest === null) {
107
+ return reply.status(400).send({
108
+ error: `Hub upload for ${version} has a missing or invalid manifest.json`,
109
+ });
110
+ }
111
+ let hubBaseUrl;
112
+ try {
113
+ hubBaseUrl = await resolveClusterBaseUrl();
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ logger.error('server-upload propagate: no hub address agents can reach', {
118
+ meta: { version, nodeId, error: message },
119
+ });
120
+ return reply.status(502).send({ error: `Agent propagate failed: ${message}` });
121
+ }
122
+ let bundle;
123
+ let source;
124
+ try {
125
+ bundle = await (0, dev_uploads_bundle_js_1.packDevUploadVersionDir)(versionDir);
126
+ source = (0, addon_upload_js_1.buildHubHttpSource)(bundle, hubBaseUrl);
127
+ }
128
+ catch (err) {
129
+ const message = err instanceof Error ? err.message : String(err);
130
+ logger.error('server-upload propagate: pack/stage failed', {
131
+ meta: { version, nodeId, error: message },
132
+ });
133
+ return reply.status(500).send({ error: `Failed to stage upload pack: ${message}` });
134
+ }
135
+ try {
136
+ const raw = await broker.call('$agent.stageDevUploads', { version, source }, { nodeID: nodeId, timeout: agent_addon_delivery_js_1.AGENT_DEPLOY_CONTROL_TIMEOUT_MS });
137
+ if (!isStageResponse(raw) || raw.success !== true) {
138
+ logger.warn('server-upload propagate: malformed agent response', {
139
+ meta: { version, nodeId },
140
+ });
141
+ return reply.status(502).send({ error: 'Agent returned a malformed stage response' });
142
+ }
143
+ const applied = raw.appliedSha256;
144
+ if (applied !== undefined && applied !== source.sha256) {
145
+ logger.error('server-upload propagate: applied hash mismatch', {
146
+ meta: { version, nodeId, expected: source.sha256, applied },
147
+ });
148
+ return reply.status(502).send({
149
+ error: `applied bundle hash mismatch (shipped ${source.sha256}, agent applied ${applied})`,
150
+ });
151
+ }
152
+ logger.info('server-upload propagate: staged on agent', {
153
+ meta: {
154
+ version,
155
+ nodeId,
156
+ path: raw.path,
157
+ packageCount: raw.packageCount,
158
+ verifiedSha256: applied,
159
+ },
160
+ });
161
+ return reply.send({
162
+ success: true,
163
+ version,
164
+ nodeId,
165
+ path: raw.path,
166
+ packageCount: raw.packageCount ?? Object.keys(manifest.packages).length,
167
+ verifiedSha256: applied,
168
+ });
169
+ }
170
+ catch (err) {
171
+ const message = err instanceof Error ? err.message : String(err);
172
+ logger.warn('server-upload propagate: agent call failed', {
173
+ meta: { version, nodeId, error: message },
174
+ });
175
+ return reply.status(502).send({ error: `Agent propagate failed: ${message}` });
176
+ }
177
+ });
178
+ }
package/dist/main.js CHANGED
@@ -71,6 +71,7 @@ const hub_cluster_url_1 = require("./api/hub-cluster-url");
71
71
  const model_distributor_js_1 = require("./api/model-distributor.js");
72
72
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
73
73
  const server_upload_1 = require("./api/server-upload");
74
+ const server_upload_propagate_1 = require("./api/server-upload-propagate");
74
75
  const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
75
76
  const frame_policy_1 = require("./api/static/frame-policy");
76
77
  const precompressed_asset_js_1 = require("./api/static/precompressed-asset.js");
@@ -453,6 +454,16 @@ async function bootstrap() {
453
454
  logger: app.get(logging_service_1.LoggingService).createLogger('server-upload'),
454
455
  });
455
456
  console.log('[bootstrap] Server upload route registered at POST /api/server-upload');
457
+ (0, server_upload_propagate_1.registerServerUploadPropagateRoute)(fastify, {
458
+ authService: uploadAuthService,
459
+ addonRegistry: uploadAddonRegistry,
460
+ logger: app.get(logging_service_1.LoggingService).createLogger('server-upload-propagate'),
461
+ broker: uploadMoleculer.broker,
462
+ resolveClusterBaseUrl: (0, hub_cluster_url_1.clusterBaseUrlResolver)(() => uploadAddonRegistry
463
+ .getCapabilityRegistry()
464
+ .getSingleton('local-network')),
465
+ });
466
+ console.log('[bootstrap] Server upload propagate route registered at POST /api/server-upload/propagate');
456
467
  // Companion endpoint: /api/auth/whoami — validates JWT or cst_*
457
468
  // scoped tokens, returns the resolved identity + scope summary.
458
469
  // Mirrors the addon-upload auth chain so the CLI can ping for
@@ -1474,7 +1474,7 @@ var require_dist = __commonJS({
1474
1474
  if (isDevChannelVersion(target)) {
1475
1475
  const uploadsDir = devUploadVersionDir(this.rootDir(), target);
1476
1476
  if (!fs6.existsSync(uploadsDir)) {
1477
- return this.refuseUpdate("dev-uploads-missing", target, `Refused: dev-channel version ${target} has no uploaded tarballs on this node (${uploadsDir} is missing). Dev server deploys are hub-only in this phase \u2014 upload via \`camstack deploy-server\` against this node first.`);
1477
+ return this.refuseUpdate("dev-uploads-missing", target, `Refused: dev-channel version ${target} has no uploaded tarballs on this node (${uploadsDir} is missing). On the hub: \`camstack deploy-server\`. On an agent: \`camstack deploy-server -n <nodeId>\` (or \`--all\`) so the hub can POST /api/server-upload/propagate first.`);
1478
1478
  }
1479
1479
  }
1480
1480
  this.inFlight = "staging";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.501",
3
+ "version": "1.2.502",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -46,7 +46,7 @@
46
46
  "@camstack/addon-pipeline-orchestrator": "1.2.305",
47
47
  "@camstack/addon-post-analysis": "1.2.352",
48
48
  "@camstack/sdk": "1.2.129",
49
- "@camstack/system": "1.2.394",
49
+ "@camstack/system": "1.2.395",
50
50
  "@camstack/types": "1.2.340",
51
51
  "@camstack/ui-library": "1.2.260",
52
52
  "@fastify/compress": "^9.0.0",