@forg3t/sdk 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/errors.ts
9
2
  var Forg3tError = class extends Error {
10
3
  constructor(message, status, code, requestId, details) {
@@ -40,18 +33,39 @@ var Forg3tApiConnectionError = class extends Forg3tError {
40
33
  this.name = "Forg3tApiConnectionError";
41
34
  }
42
35
  };
36
+ var Forg3tNotImplementedError = class extends Forg3tError {
37
+ constructor(message, endpoint, requestId) {
38
+ super(
39
+ `Endpoint not implemented: ${endpoint || "unknown"}`,
40
+ 501,
41
+ "NOT_IMPLEMENTED",
42
+ requestId,
43
+ { endpoint }
44
+ );
45
+ this.name = "Forg3tNotImplementedError";
46
+ }
47
+ };
43
48
 
44
49
  // src/transport.ts
45
50
  var Transport = class {
46
51
  constructor(config) {
47
- this.baseUrl = config.baseUrl || typeof process !== "undefined" && process.env.FORG3T_API_URL || "";
52
+ const rawBaseUrl = config.baseUrl || typeof process !== "undefined" && process.env.FORG3T_API_URL || "";
53
+ this.baseUrl = String(rawBaseUrl).trim();
48
54
  if (!this.baseUrl) {
49
55
  throw new Error("Forg3tClient: apiUrl option or FORG3T_API_URL environment variable is required");
50
56
  }
57
+ if (!/^https?:\/\//i.test(this.baseUrl)) {
58
+ throw new Error("Forg3tClient: apiUrl must be an absolute http(s) URL");
59
+ }
51
60
  if (this.baseUrl.endsWith("/")) {
52
61
  this.baseUrl = this.baseUrl.slice(0, -1);
53
62
  }
54
- this.apiKey = config.apiKey || (typeof process !== "undefined" ? process.env.FORG3T_API_KEY : void 0);
63
+ const rawApiKey = config.apiKey || (typeof process !== "undefined" ? process.env.FORG3T_API_KEY : void 0);
64
+ const apiKey = typeof rawApiKey === "string" ? rawApiKey.trim() : rawApiKey;
65
+ this.apiKey = apiKey || void 0;
66
+ const rawBearerToken = config.bearerToken || (typeof process !== "undefined" ? process.env.FORG3T_BEARER_TOKEN : void 0);
67
+ const bearerToken = typeof rawBearerToken === "string" ? rawBearerToken.trim() : rawBearerToken;
68
+ this.bearerToken = bearerToken || void 0;
55
69
  this.timeoutMs = config.timeoutMs || 3e4;
56
70
  }
57
71
  getBaseUrl() {
@@ -60,15 +74,22 @@ var Transport = class {
60
74
  async request(path, options = {}, requestId) {
61
75
  const url = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
62
76
  const headers = {
63
- "Content-Type": "application/json",
64
77
  "Accept": "application/json",
65
78
  ...options.headers || {}
66
79
  };
80
+ const hasExplicitContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
81
+ const hasBody = options.body !== void 0 && options.body !== null;
82
+ if (hasBody && !hasExplicitContentType) {
83
+ headers["Content-Type"] = "application/json";
84
+ }
67
85
  if (this.apiKey) {
68
86
  headers["x-api-key"] = this.apiKey;
69
87
  }
88
+ if (this.bearerToken && !headers.Authorization) {
89
+ headers.Authorization = `Bearer ${this.bearerToken}`;
90
+ }
70
91
  if (requestId) {
71
- headers["x-request-id"] = requestId;
92
+ headers["x-correlation-id"] = requestId;
72
93
  }
73
94
  const controller = new AbortController();
74
95
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -81,7 +102,7 @@ var Transport = class {
81
102
  // Add the abort signal
82
103
  });
83
104
  clearTimeout(timeoutId);
84
- const responseRequestId = response.headers.get("x-request-id") || requestId;
105
+ const responseRequestId = response.headers.get("x-correlation-id") || response.headers.get("x-request-id") || requestId;
85
106
  if (!response.ok) {
86
107
  let errorData = {};
87
108
  try {
@@ -119,7 +140,8 @@ var Transport = class {
119
140
  if (error instanceof Error && error.name === "AbortError") {
120
141
  throw new Forg3tApiConnectionError(`Request timeout after ${this.timeoutMs}ms`);
121
142
  }
122
- throw new Forg3tApiConnectionError(error instanceof Error ? error.message : "Network error");
143
+ const detail = error instanceof Error ? error.message : "Network error";
144
+ throw new Forg3tApiConnectionError(`Request failed for ${url}: ${detail}`);
123
145
  }
124
146
  }
125
147
  async download(url, requestId) {
@@ -130,6 +152,9 @@ var Transport = class {
130
152
  if (this.apiKey) {
131
153
  headers["x-api-key"] = this.apiKey;
132
154
  }
155
+ if (this.bearerToken && !headers.Authorization) {
156
+ headers.Authorization = `Bearer ${this.bearerToken}`;
157
+ }
133
158
  if (requestId) {
134
159
  headers["x-request-id"] = requestId;
135
160
  }
@@ -178,7 +203,8 @@ var Transport = class {
178
203
  if (error instanceof Error && error.name === "AbortError") {
179
204
  throw new Forg3tApiConnectionError(`Download timeout after ${this.timeoutMs}ms`);
180
205
  }
181
- throw new Forg3tApiConnectionError(error instanceof Error ? error.message : "Network error");
206
+ const detail = error instanceof Error ? error.message : "Network error";
207
+ throw new Forg3tApiConnectionError(`Download failed for ${url}: ${detail}`);
182
208
  }
183
209
  }
184
210
  async requestRaw(path, options = {}, requestId) {
@@ -191,6 +217,9 @@ var Transport = class {
191
217
  if (this.apiKey) {
192
218
  headers["x-api-key"] = this.apiKey;
193
219
  }
220
+ if (this.bearerToken && !headers.Authorization) {
221
+ headers.Authorization = `Bearer ${this.bearerToken}`;
222
+ }
194
223
  if (requestId) {
195
224
  headers["x-request-id"] = requestId;
196
225
  }
@@ -214,7 +243,8 @@ var Transport = class {
214
243
  if (error instanceof Error && error.name === "AbortError") {
215
244
  throw new Forg3tApiConnectionError(`Request timeout after ${this.timeoutMs}ms`);
216
245
  }
217
- throw new Forg3tApiConnectionError(error instanceof Error ? error.message : "Network error");
246
+ const detail = error instanceof Error ? error.message : "Network error";
247
+ throw new Forg3tApiConnectionError(`Raw request failed for ${url}: ${detail}`);
218
248
  }
219
249
  }
220
250
  };
@@ -225,9 +255,31 @@ var Forg3tClient = class {
225
255
  this.transport = new Transport({
226
256
  baseUrl: options.apiUrl,
227
257
  apiKey: options.apiKey,
258
+ bearerToken: options.bearerToken,
228
259
  timeoutMs: options.timeoutMs
229
260
  });
230
261
  }
262
+ // --- Authentication ---
263
+ /**
264
+ * Get the current user context and their default project.
265
+ * Used by the Admin Dashboard for bootstrap and session verification.
266
+ */
267
+ async getCurrentUser(requestId) {
268
+ return this.transport.request("/v1/me", { method: "GET" }, requestId);
269
+ }
270
+ /**
271
+ * Bootstraps a tenant for an authenticated dashboard user.
272
+ * Requires a bearer token from the user's session instead of an API key.
273
+ */
274
+ async bootstrapTenant(data, requestId) {
275
+ if (!data?.tenantName || typeof data.tenantName !== "string" || !data.tenantName.trim()) {
276
+ throw new Error("bootstrapTenant requires a non-empty tenantName.");
277
+ }
278
+ return this.transport.request("/v1/bootstrap/tenant", {
279
+ method: "POST",
280
+ body: JSON.stringify(data)
281
+ }, requestId);
282
+ }
231
283
  // --- Projects ---
232
284
  async getProjectOverview(projectId, requestId) {
233
285
  return this.transport.request(`/v1/projects/${projectId}/overview`, { method: "GET" }, requestId);
@@ -243,6 +295,42 @@ var Forg3tClient = class {
243
295
  if (filters.cursor) params.append("cursor", filters.cursor);
244
296
  return this.transport.request(`/v1/projects/${projectId}/audit?${params.toString()}`, { method: "GET" }, requestId);
245
297
  }
298
+ // --- Integrations ---
299
+ async listIntegrations(projectId, requestId) {
300
+ return this.transport.request(`/v1/projects/${projectId}/integrations`, { method: "GET" }, requestId);
301
+ }
302
+ async createIntegration(projectId, data, requestId) {
303
+ return this.transport.request(`/v1/projects/${projectId}/integrations`, {
304
+ method: "POST",
305
+ body: JSON.stringify(data)
306
+ }, requestId);
307
+ }
308
+ async testIntegration(projectId, integrationId, requestId) {
309
+ void projectId;
310
+ return this.transport.request(
311
+ `/v1/integrations/${integrationId}/test`,
312
+ { method: "POST" },
313
+ requestId
314
+ );
315
+ }
316
+ // --- Unlearning Requests ---
317
+ async createUnlearningRequest(projectId, data, requestId) {
318
+ this.assertCreateUnlearningRequest(data);
319
+ return this.transport.request(`/v1/projects/${projectId}/unlearning-requests`, {
320
+ method: "POST",
321
+ body: JSON.stringify(data)
322
+ }, requestId);
323
+ }
324
+ async listUnlearningRequests(projectId, requestId) {
325
+ return this.transport.request(`/v1/projects/${projectId}/unlearning-requests`, { method: "GET" }, requestId);
326
+ }
327
+ async getUnlearningRequest(projectId, unlearningRequestId, requestId) {
328
+ return this.transport.request(
329
+ `/v1/projects/${projectId}/unlearning-requests/${unlearningRequestId}`,
330
+ { method: "GET" },
331
+ requestId
332
+ );
333
+ }
246
334
  // --- API Keys ---
247
335
  async listApiKeys(projectId, requestId) {
248
336
  return this.transport.request(`/v1/projects/${projectId}/api-keys`, { method: "GET" }, requestId);
@@ -253,23 +341,74 @@ var Forg3tClient = class {
253
341
  body: JSON.stringify(data)
254
342
  }, requestId);
255
343
  }
256
- async rotateApiKey(keyId, requestId) {
257
- return this.transport.request(`/v1/api-keys/${keyId}/rotate`, { method: "POST" }, requestId);
344
+ async rotateApiKey(projectId, keyId, requestId) {
345
+ if (!projectId || !keyId) {
346
+ throw new Error("rotateApiKey requires both projectId and keyId.");
347
+ }
348
+ const existingKeys = await this.listApiKeys(projectId, requestId);
349
+ const current = existingKeys.find((item) => item.id === keyId);
350
+ if (!current) {
351
+ throw new Forg3tNotFoundError(`API key ${keyId} was not found in project ${projectId}.`, requestId);
352
+ }
353
+ const rotated = await this.createApiKey(projectId, {
354
+ name: current.name,
355
+ ...current.expiresAt ? { expiresAt: current.expiresAt } : {}
356
+ }, requestId);
357
+ await this.revokeApiKey(projectId, keyId, requestId);
358
+ return rotated;
258
359
  }
259
- async revokeApiKey(keyId, requestId) {
260
- return this.transport.request(`/v1/api-keys/${keyId}/revoke`, { method: "POST" }, requestId);
360
+ async revokeApiKey(projectId, keyId, requestId) {
361
+ if (!projectId || !keyId) {
362
+ throw new Error("revokeApiKey requires both projectId and keyId.");
363
+ }
364
+ return this.transport.request(`/v1/projects/${projectId}/api-keys/${keyId}`, {
365
+ method: "DELETE"
366
+ }, requestId);
261
367
  }
262
368
  // --- Jobs ---
263
369
  async submitJob(projectId, data, requestId) {
370
+ const normalizedPayload = "payload" in data ? data.payload : {
371
+ claim: data.claim,
372
+ config: data.config
373
+ };
374
+ if (data.type === "MODEL_UNLEARN") {
375
+ this.assertModelUnlearnPreflight(normalizedPayload);
376
+ }
264
377
  const res = await this.transport.request(`/v1/projects/${projectId}/jobs`, {
265
378
  method: "POST",
266
- body: JSON.stringify(data)
379
+ body: JSON.stringify({
380
+ type: data.type,
381
+ payload: normalizedPayload,
382
+ idempotencyKey: data.idempotencyKey
383
+ })
267
384
  }, requestId);
268
385
  return res;
269
386
  }
387
+ /**
388
+ * Creates a new job with deduplication support via an optional idempotency key.
389
+ * This wrapper cleanly exposes the core parameters for the production job queue.
390
+ */
391
+ async createJob(projectId, type, claim, config, idempotencyKey, requestId) {
392
+ return this.submitJob(projectId, { type, claim, config, idempotencyKey }, requestId);
393
+ }
270
394
  async getJob(projectId, jobId, requestId) {
271
395
  return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}`, { method: "GET" }, requestId);
272
396
  }
397
+ /**
398
+ * Polls the job status continuously until a terminal state (succeeded, failed, canceled) is reached,
399
+ * or the specified timeout limits are exceeded.
400
+ */
401
+ async pollJobStatus(projectId, jobId, timeoutMs = 3e5, pollIntervalMs = 2e3, requestId) {
402
+ const startTime = Date.now();
403
+ while (Date.now() - startTime < timeoutMs) {
404
+ const job = await this.getJob(projectId, jobId, requestId);
405
+ if (job.status === "succeeded" || job.status === "failed" || job.status === "canceled") {
406
+ return job;
407
+ }
408
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
409
+ }
410
+ throw new Error(`pollJobStatus timed out after ${timeoutMs}ms waiting for job ${jobId} to reach a terminal state.`);
411
+ }
273
412
  async listJobs(projectId, query, requestId) {
274
413
  const params = new URLSearchParams();
275
414
  if (query?.status) params.append("status", query.status);
@@ -283,20 +422,37 @@ var Forg3tClient = class {
283
422
  body: JSON.stringify(data)
284
423
  }, requestId);
285
424
  }
286
- async heartbeatJob(projectId, jobId, requestId) {
287
- return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/heartbeat`, { method: "POST" }, requestId);
425
+ async heartbeatJob(projectId, jobId, optionsOrRequestId, requestId) {
426
+ const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
427
+ const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
428
+ return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/heartbeat`, {
429
+ method: "POST",
430
+ body: JSON.stringify({
431
+ claimId: options.claimId
432
+ })
433
+ }, resolvedRequestId);
288
434
  }
289
- async completeJob(projectId, jobId, result, requestId) {
435
+ async completeJob(projectId, jobId, result, optionsOrRequestId, requestId) {
436
+ const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
437
+ const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
290
438
  return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/complete`, {
291
439
  method: "POST",
292
- body: JSON.stringify(result)
293
- }, requestId);
440
+ body: JSON.stringify({
441
+ ...result,
442
+ claimId: options.claimId || result.claimId
443
+ })
444
+ }, resolvedRequestId);
294
445
  }
295
- async failJob(projectId, jobId, error, requestId) {
446
+ async failJob(projectId, jobId, error, optionsOrRequestId, requestId) {
447
+ const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
448
+ const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
296
449
  return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/fail`, {
297
450
  method: "POST",
298
- body: JSON.stringify(error)
299
- }, requestId);
451
+ body: JSON.stringify({
452
+ ...error,
453
+ claimId: options.claimId || error.claimId
454
+ })
455
+ }, resolvedRequestId);
300
456
  }
301
457
  async listDeadJobs(projectId, requestId) {
302
458
  return this.transport.request(`/v1/projects/${projectId}/jobs/dead`, { method: "GET" }, requestId);
@@ -306,7 +462,20 @@ var Forg3tClient = class {
306
462
  }
307
463
  // --- Evidence ---
308
464
  async getEvidence(jobId, requestId) {
309
- return this.transport.request(`/v1/jobs/evidence/${jobId}`, { method: "GET" }, requestId);
465
+ try {
466
+ return await this.transport.request(`/v1/jobs/${jobId}/evidence`, { method: "GET" }, requestId);
467
+ } catch (error) {
468
+ if (error instanceof Forg3tNotFoundError) {
469
+ return this.transport.request(`/v1/jobs/evidence/${jobId}`, { method: "GET" }, requestId);
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ async getEvidenceJson(jobId, requestId) {
475
+ return this.transport.request(`/v1/jobs/${jobId}/evidence.json`, { method: "GET" }, requestId);
476
+ }
477
+ async getArtifactDownloadUrl(projectId, jobId, filename, requestId) {
478
+ return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/artifacts/${filename}/download`, { method: "GET" }, requestId);
310
479
  }
311
480
  async getEvidencePdf(jobId, options) {
312
481
  const response = await this.transport.requestRaw(`/v1/jobs/${jobId}/evidence.pdf`, { method: "GET" });
@@ -330,26 +499,24 @@ var Forg3tClient = class {
330
499
  return result;
331
500
  }
332
501
  async savePdfToFile(data, filePath) {
333
- if (typeof global !== "undefined" && typeof __require !== "undefined") {
334
- try {
335
- const fs = __require("fs");
336
- const path = __require("path");
337
- const dir = path.dirname(filePath);
338
- if (!fs.existsSync(dir)) {
339
- fs.mkdirSync(dir, { recursive: true });
340
- }
341
- fs.writeFileSync(filePath, Buffer.from(data));
342
- } catch (error) {
343
- console.error("Failed to save PDF to file:", error);
344
- throw error;
345
- }
346
- } else {
502
+ const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
503
+ if (!isNode) {
347
504
  throw new Error("File saving is only supported in Node.js environment");
348
505
  }
506
+ try {
507
+ const fs = await import("fs/promises");
508
+ const path = await import("path");
509
+ const dir = path.dirname(filePath);
510
+ await fs.mkdir(dir, { recursive: true });
511
+ await fs.writeFile(filePath, data);
512
+ } catch (error) {
513
+ console.error("Failed to save PDF to file:", error);
514
+ throw error;
515
+ }
349
516
  }
350
517
  /**
351
518
  * @deprecated The current Control Plane version does not support direct artifact downloads.
352
- * Please use getEvidence() to retrieve the artifact metadata and content.
519
+ * Please use getArtifactDownloadUrl(), getEvidencePdf(), or getEvidenceJson().
353
520
  */
354
521
  async downloadEvidenceArtifact(_jobId, _requestId) {
355
522
  throw new Error("Artifact download not supported by current API version. Use getEvidence() to retrieve content.");
@@ -420,12 +587,904 @@ var Forg3tClient = class {
420
587
  body: JSON.stringify(result)
421
588
  }, requestId);
422
589
  }
590
+ assertCreateUnlearningRequest(data) {
591
+ if (!data.target?.value?.trim()) {
592
+ throw new Error("createUnlearningRequest: target.value is required.");
593
+ }
594
+ if (typeof data.target.aliases !== "undefined") {
595
+ if (!Array.isArray(data.target.aliases) || data.target.aliases.some((alias) => typeof alias !== "string" || !alias.trim())) {
596
+ throw new Error("createUnlearningRequest: target.aliases must be an array of non-empty strings when provided.");
597
+ }
598
+ }
599
+ if (data.accessLevel === "layer_a_only") {
600
+ const target = data.execution?.target;
601
+ if (!target) {
602
+ throw new Error("createUnlearningRequest: execution.target is required for layer_a_only.");
603
+ }
604
+ if ((target.provider === "openai" || target.provider === "groq") && !target.model?.trim()) {
605
+ throw new Error(`createUnlearningRequest: execution.target.model is required when provider=${target.provider}.`);
606
+ }
607
+ if ((target.provider === "http_generic" || target.provider === "custom") && !target.endpoint?.trim()) {
608
+ throw new Error(`createUnlearningRequest: execution.target.endpoint is required when provider=${target.provider}.`);
609
+ }
610
+ }
611
+ if (data.accessLevel === "layer_a_and_b") {
612
+ if (!data.execution?.model?.uri?.trim()) {
613
+ throw new Error("createUnlearningRequest: execution.model.uri is required for layer_a_and_b.");
614
+ }
615
+ const targetTokenIds = data.execution?.plan?.hyperparameters?.target_token_ids;
616
+ const tokenizerUri = data.execution?.model?.metadata?.tokenizer_uri;
617
+ const localizationMode = String(
618
+ data.execution?.plan?.hyperparameters?.localization_mode || data.execution?.plan?.hyperparameters?.localizationMode || ""
619
+ ).trim().toLowerCase();
620
+ const allowMissingLocalization = String(process.env.FORG3T_ALLOW_MODEL_UNLEARN_WITHOUT_LOCALIZATION_INPUT || "").toLowerCase() === "true";
621
+ const hasTokenIds = Array.isArray(targetTokenIds) && targetTokenIds.some((value) => Number.isInteger(value) && Number(value) >= 0);
622
+ const hasTokenizer = typeof tokenizerUri === "string" && tokenizerUri.trim().length > 0;
623
+ if (localizationMode === "explicit_token_ids" && !hasTokenIds) {
624
+ throw new Error("createUnlearningRequest: localization_mode=explicit_token_ids requires target token IDs.");
625
+ }
626
+ if (localizationMode === "tokenizer_auto" && !hasTokenizer) {
627
+ throw new Error("createUnlearningRequest: localization_mode=tokenizer_auto requires execution.model.metadata.tokenizer_uri.");
628
+ }
629
+ if (localizationMode === "hybrid") {
630
+ if (!hasTokenIds) {
631
+ throw new Error("createUnlearningRequest: localization_mode=hybrid requires target token IDs.");
632
+ }
633
+ if (!hasTokenizer) {
634
+ throw new Error("createUnlearningRequest: localization_mode=hybrid requires execution.model.metadata.tokenizer_uri.");
635
+ }
636
+ }
637
+ if (!allowMissingLocalization && !hasTokenIds && !hasTokenizer) {
638
+ throw new Error(
639
+ "createUnlearningRequest: layer_a_and_b requires localization input. Provide target token IDs (execution.plan.hyperparameters.target_token_ids) or tokenizer URI (execution.model.metadata.tokenizer_uri)."
640
+ );
641
+ }
642
+ }
643
+ }
644
+ assertModelUnlearnPreflight(payload) {
645
+ if (!payload || typeof payload !== "object") {
646
+ throw new Error("MODEL_UNLEARN preflight failed: payload must be an object.");
647
+ }
648
+ const asRecord = payload;
649
+ const config = asRecord.config && typeof asRecord.config === "object" && !Array.isArray(asRecord.config) ? asRecord.config : null;
650
+ if (!config) {
651
+ throw new Error("MODEL_UNLEARN preflight failed: payload.config is required.");
652
+ }
653
+ const targetConfig = config.target_config && typeof config.target_config === "object" && !Array.isArray(config.target_config) ? config.target_config : null;
654
+ const parameters = config.parameters && typeof config.parameters === "object" && !Array.isArray(config.parameters) ? config.parameters : null;
655
+ const modelFromTargetConfig = targetConfig?.model && typeof targetConfig.model === "object" && !Array.isArray(targetConfig.model) ? targetConfig.model : null;
656
+ const modelFromParameters = parameters?.model && typeof parameters.model === "object" && !Array.isArray(parameters.model) ? parameters.model : null;
657
+ const model = modelFromParameters || modelFromTargetConfig;
658
+ const modelUri = typeof model?.uri === "string" ? model.uri.trim() : "";
659
+ if (!modelUri) {
660
+ throw new Error("MODEL_UNLEARN preflight failed: config.target_config.model.uri (or config.parameters.model.uri) is required.");
661
+ }
662
+ const target = parameters?.target && typeof parameters.target === "object" && !Array.isArray(parameters.target) ? parameters.target : null;
663
+ const tokenIds = target?.tokenIds;
664
+ const hasTokenIds = Array.isArray(tokenIds) && tokenIds.some((value) => Number.isInteger(value) && Number(value) >= 0);
665
+ const tokenizerFromTargetConfig = targetConfig?.tokenizer && typeof targetConfig.tokenizer === "object" && !Array.isArray(targetConfig.tokenizer) ? targetConfig.tokenizer : null;
666
+ const tokenizerFromParameters = parameters?.tokenizer && typeof parameters.tokenizer === "object" && !Array.isArray(parameters.tokenizer) ? parameters.tokenizer : null;
667
+ const tokenizerFromModel = model?.tokenizer && typeof model.tokenizer === "object" && !Array.isArray(model.tokenizer) ? model.tokenizer : null;
668
+ const tokenizerCandidates = [
669
+ tokenizerFromTargetConfig?.uri,
670
+ tokenizerFromParameters?.uri,
671
+ parameters?.tokenizer_uri,
672
+ model?.tokenizer_uri,
673
+ tokenizerFromModel?.uri
674
+ ];
675
+ const hasTokenizerUri = tokenizerCandidates.some((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
676
+ const allowMissingLocalization = String(process.env.FORG3T_ALLOW_MODEL_UNLEARN_WITHOUT_LOCALIZATION_INPUT || "").toLowerCase() === "true";
677
+ if (!allowMissingLocalization && !hasTokenIds && !hasTokenizerUri) {
678
+ throw new Error(
679
+ "MODEL_UNLEARN preflight failed: missing localization input. Provide config.parameters.target.tokenIds or a tokenizer URI in config.target_config.tokenizer.uri / config.parameters.tokenizer.uri / config.parameters.tokenizer_uri / model.tokenizer_uri."
680
+ );
681
+ }
682
+ }
683
+ };
684
+
685
+ // src/types.ts
686
+ var IntegrationType = /* @__PURE__ */ ((IntegrationType2) => {
687
+ IntegrationType2["SLACK"] = "slack";
688
+ IntegrationType2["CONFLUENCE"] = "confluence";
689
+ IntegrationType2["GOOGLE_DRIVE"] = "google_drive";
690
+ IntegrationType2["ZENDESK"] = "zendesk";
691
+ IntegrationType2["NOTION"] = "notion";
692
+ IntegrationType2["LANGCHAIN"] = "langchain";
693
+ IntegrationType2["OPENROUTER"] = "openrouter";
694
+ IntegrationType2["OPENAI"] = "openai";
695
+ IntegrationType2["CUSTOM"] = "custom";
696
+ return IntegrationType2;
697
+ })(IntegrationType || {});
698
+ var IntegrationMode = /* @__PURE__ */ ((IntegrationMode2) => {
699
+ IntegrationMode2["MODE_A_BLACKBOX"] = "mode_a_blackbox";
700
+ IntegrationMode2["MODE_B_WHITEBOX"] = "mode_b_whitebox";
701
+ return IntegrationMode2;
702
+ })(IntegrationMode || {});
703
+ var AccessLevel = /* @__PURE__ */ ((AccessLevel2) => {
704
+ AccessLevel2["LAYER_A_ONLY"] = "layer_a_only";
705
+ AccessLevel2["LAYER_A_AND_B"] = "layer_a_and_b";
706
+ return AccessLevel2;
707
+ })(AccessLevel || {});
708
+
709
+ // src/whitebox.ts
710
+ import * as crypto from "crypto";
711
+
712
+ // src/quality-gate.ts
713
+ var WHITEBOX_QUALITY_GATE_PRESETS = {
714
+ balanced: {
715
+ requireEvaluation: false,
716
+ maxLeakScore: 0.05,
717
+ allowedVerdicts: ["PASS", "REVIEW"],
718
+ minChangedTensorCount: 1,
719
+ minChangedParamCount: 1,
720
+ requireModelHashTransition: true,
721
+ requireLocalization: false,
722
+ minLocalizationTokenCoverage: 0.5,
723
+ minLocalizedParamCount: 1,
724
+ minLocalizationConfidence: 0.6
725
+ },
726
+ strict: {
727
+ requireEvaluation: true,
728
+ maxLeakScore: 0.03,
729
+ allowedVerdicts: ["PASS"],
730
+ minChangedTensorCount: 1,
731
+ minChangedParamCount: 1,
732
+ requireModelHashTransition: true,
733
+ requireLocalization: true,
734
+ minLocalizationTokenCoverage: 0.5,
735
+ minLocalizedParamCount: 1,
736
+ minLocalizationConfidence: 0.6
737
+ }
738
+ };
739
+ var WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS = {
740
+ balanced: "Balanced gate for faster rollout. Allows PASS/REVIEW and can run without evaluator output.",
741
+ strict: "Strict gate for production. Requires evaluator output, localization evidence, and PASS verdict only."
742
+ };
743
+ var DEFAULT_WHITEBOX_QUALITY_GATE_POLICY = {
744
+ requireEvaluation: false,
745
+ maxLeakScore: 0.05,
746
+ allowedVerdicts: ["PASS", "REVIEW"],
747
+ minChangedTensorCount: 1,
748
+ minChangedParamCount: 1,
749
+ requireModelHashTransition: true,
750
+ requireLocalization: false,
751
+ minLocalizationTokenCoverage: 0.5,
752
+ minLocalizedParamCount: 1,
753
+ minLocalizationConfidence: 0.6
754
+ };
755
+ var resolveWhiteboxQualityGatePolicy = (preset = "balanced", overrides) => {
756
+ return {
757
+ ...WHITEBOX_QUALITY_GATE_PRESETS[preset],
758
+ ...overrides || {}
759
+ };
760
+ };
761
+ var evaluateWhiteboxQualityGate = (unlearning, evaluation, policyOverrides) => {
762
+ const policy = {
763
+ ...resolveWhiteboxQualityGatePolicy("balanced"),
764
+ ...policyOverrides || {}
765
+ };
766
+ const reasons = [];
767
+ const changedTensors = Number(unlearning.changedTensorCount || 0);
768
+ const changedParams = Number(unlearning.changedParamCount || 0);
769
+ const minChangedTensors = Number(policy.minChangedTensorCount || 0);
770
+ const minChangedParams = Number(policy.minChangedParamCount || 0);
771
+ if (changedTensors < minChangedTensors) {
772
+ reasons.push(`Changed tensor count ${changedTensors} is below minimum ${minChangedTensors}.`);
773
+ }
774
+ if (minChangedParams > 0 && changedParams < minChangedParams) {
775
+ reasons.push(`Changed parameter count ${changedParams} is below minimum ${minChangedParams}.`);
776
+ }
777
+ if (policy.requireModelHashTransition) {
778
+ const before = (unlearning.modelBeforeSha256 || "").trim();
779
+ const after = (unlearning.modelAfterSha256 || "").trim();
780
+ if (!before || !after) {
781
+ reasons.push("Model hash transition is required but before/after hash is missing.");
782
+ } else if (before === after) {
783
+ reasons.push("Model hash did not change after unlearning.");
784
+ }
785
+ }
786
+ if (policy.requireEvaluation && !evaluation) {
787
+ reasons.push("Evaluation output is required but missing.");
788
+ }
789
+ if (policy.requireLocalization) {
790
+ const localization = unlearning.localization;
791
+ if (!localization) {
792
+ reasons.push("Localization output is required but missing.");
793
+ } else {
794
+ const tokenCoverage = Number(localization.tokenCoverage);
795
+ if (!Number.isFinite(tokenCoverage) || tokenCoverage < 0 || tokenCoverage > 1) {
796
+ reasons.push("Localization tokenCoverage must be a finite number in [0, 1].");
797
+ } else if (tokenCoverage < Number(policy.minLocalizationTokenCoverage)) {
798
+ reasons.push(
799
+ `Localization tokenCoverage ${tokenCoverage} is below minimum ${policy.minLocalizationTokenCoverage}.`
800
+ );
801
+ }
802
+ const localizedParamCount = Number(localization.localizedParamCount || 0);
803
+ if (localizedParamCount < Number(policy.minLocalizedParamCount)) {
804
+ reasons.push(
805
+ `Localization localizedParamCount ${localizedParamCount} is below minimum ${policy.minLocalizedParamCount}.`
806
+ );
807
+ }
808
+ const confidence = Number(localization.confidence);
809
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
810
+ reasons.push("Localization confidence must be a finite number in [0, 1].");
811
+ } else if (confidence < Number(policy.minLocalizationConfidence)) {
812
+ reasons.push(
813
+ `Localization confidence ${confidence} is below minimum ${policy.minLocalizationConfidence}.`
814
+ );
815
+ }
816
+ }
817
+ }
818
+ if (evaluation) {
819
+ const maxLeak = Number(policy.maxLeakScore);
820
+ if (!Number.isNaN(maxLeak) && evaluation.leakScore > maxLeak) {
821
+ reasons.push(`Leak score ${evaluation.leakScore} is above max ${maxLeak}.`);
822
+ }
823
+ const allowedVerdicts = policy.allowedVerdicts || [];
824
+ if (allowedVerdicts.length > 0 && !allowedVerdicts.includes(evaluation.finalVerdict)) {
825
+ reasons.push(`Final verdict ${evaluation.finalVerdict} is not allowed by policy.`);
826
+ }
827
+ }
828
+ return {
829
+ pass: reasons.length === 0,
830
+ reasons,
831
+ policy
832
+ };
833
+ };
834
+
835
+ // src/whitebox.ts
836
+ var buildModelUnlearningPayload = (input) => {
837
+ return {
838
+ claim: input.claim,
839
+ config: {
840
+ target_adapter: input.targetAdapter || "CUSTOM_WHITEBOX_BACKEND",
841
+ target_config: {
842
+ model: input.model
843
+ },
844
+ sensitivity: input.sensitivity || "high",
845
+ parameters: {
846
+ target: input.target,
847
+ plan: input.plan,
848
+ ...input.extraParameters
849
+ }
850
+ }
851
+ };
852
+ };
853
+ var defaultLogger = {
854
+ info(message, meta) {
855
+ console.log(`[whitebox-worker] ${message}`, meta || {});
856
+ },
857
+ warn(message, meta) {
858
+ console.warn(`[whitebox-worker] ${message}`, meta || {});
859
+ },
860
+ error(message, meta) {
861
+ console.error(`[whitebox-worker] ${message}`, meta || {});
862
+ }
863
+ };
864
+ var safeErrorMessage = (error) => {
865
+ if (error instanceof Error) {
866
+ return error.message.slice(0, 500);
867
+ }
868
+ return String(error).slice(0, 500);
423
869
  };
870
+ var toJobError = (error) => {
871
+ const message = safeErrorMessage(error);
872
+ const isValidationFailure = message.startsWith("VALIDATION_ERROR:") || message.startsWith("QUALITY_GATE_FAILED:") || message.startsWith("ADAPTER_OUTPUT_INVALID:");
873
+ return {
874
+ error: {
875
+ code: isValidationFailure ? "VALIDATION_ERROR" : "WORKER_ERROR",
876
+ message
877
+ }
878
+ };
879
+ };
880
+ var toClaim = (job) => {
881
+ const claim = job.payload?.claim;
882
+ if (!claim) {
883
+ throw new Error("VALIDATION_ERROR: job payload.claim is required.");
884
+ }
885
+ if (typeof claim.claim_payload !== "string" || !claim.claim_payload.trim()) {
886
+ throw new Error("VALIDATION_ERROR: job payload.claim.claim_payload must be a non-empty string.");
887
+ }
888
+ return {
889
+ claim_type: claim.claim_type || "CONCEPT",
890
+ claim_payload: claim.claim_payload,
891
+ scope: claim.scope || "global",
892
+ assertion: claim.assertion || "must_not_be_recalled"
893
+ };
894
+ };
895
+ var toConfig = (job) => {
896
+ const config = job.payload?.config;
897
+ if (!config) {
898
+ throw new Error("VALIDATION_ERROR: job payload.config is required.");
899
+ }
900
+ return {
901
+ target_adapter: config.target_adapter || "CUSTOM_WHITEBOX_BACKEND",
902
+ target_config: config.target_config || {},
903
+ sensitivity: config.sensitivity || "high",
904
+ parameters: config.parameters || {}
905
+ };
906
+ };
907
+ var resolveModel = (config) => {
908
+ const modelFromTargetConfig = config.target_config?.model;
909
+ const modelFromParameters = config.parameters?.model;
910
+ const model = modelFromParameters || modelFromTargetConfig;
911
+ if (!model || !model.uri) {
912
+ throw new Error("MODEL_UNLEARN job missing model URI in config.target_config.model or config.parameters.model");
913
+ }
914
+ return model;
915
+ };
916
+ var resolveTarget = (claim, config) => {
917
+ const targetFromParameters = config.parameters?.target;
918
+ if (targetFromParameters) {
919
+ return targetFromParameters;
920
+ }
921
+ return {
922
+ text: claim.claim_payload,
923
+ selector: {
924
+ claim_type: claim.claim_type,
925
+ scope: claim.scope
926
+ }
927
+ };
928
+ };
929
+ var resolvePlan = (config) => {
930
+ const planFromParameters = config.parameters?.plan;
931
+ if (planFromParameters?.method) {
932
+ return planFromParameters;
933
+ }
934
+ const fallbackMethod = typeof config.parameters?.method === "string" ? config.parameters.method : "gradient_surgery";
935
+ const fallbackHyperparams = config.parameters?.hyperparameters || {};
936
+ return {
937
+ method: fallbackMethod,
938
+ hyperparameters: fallbackHyperparams
939
+ };
940
+ };
941
+ var isNonArrayRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
942
+ var resolveQualityGatePresetCandidate = (value) => {
943
+ if (typeof value !== "string") {
944
+ return void 0;
945
+ }
946
+ const normalized = value.trim().toLowerCase();
947
+ if (normalized === "strict" || normalized === "balanced") {
948
+ return normalized;
949
+ }
950
+ return void 0;
951
+ };
952
+ var resolvePerJobQualityGatePolicy = (workerPolicy, config, plan) => {
953
+ const configParams = isNonArrayRecord(config.parameters) ? config.parameters : {};
954
+ const planHyperparameters = isNonArrayRecord(plan.hyperparameters) ? plan.hyperparameters : {};
955
+ const preset = resolveQualityGatePresetCandidate(
956
+ planHyperparameters.quality_gate_preset || planHyperparameters.qualityGatePreset || configParams.quality_gate_preset || configParams.qualityGatePreset
957
+ );
958
+ const overrideFromPlan = isNonArrayRecord(planHyperparameters.quality_gate_policy) ? planHyperparameters.quality_gate_policy : isNonArrayRecord(planHyperparameters.qualityGatePolicy) ? planHyperparameters.qualityGatePolicy : {};
959
+ const overrideFromConfig = isNonArrayRecord(configParams.quality_gate_policy) ? configParams.quality_gate_policy : isNonArrayRecord(configParams.qualityGatePolicy) ? configParams.qualityGatePolicy : {};
960
+ const hasOverrides = Object.keys(overrideFromPlan).length > 0 || Object.keys(overrideFromConfig).length > 0;
961
+ if (!preset && !hasOverrides) {
962
+ return {
963
+ policy: workerPolicy,
964
+ preset: null
965
+ };
966
+ }
967
+ const presetPolicy = preset ? resolveWhiteboxQualityGatePolicy(preset) : workerPolicy;
968
+ return {
969
+ policy: {
970
+ ...presetPolicy,
971
+ ...overrideFromConfig,
972
+ ...overrideFromPlan
973
+ },
974
+ preset: preset || null
975
+ };
976
+ };
977
+ var assertUnlearningOutput = (value) => {
978
+ const updatedModel = value?.updatedModel;
979
+ if (!updatedModel || typeof updatedModel.uri !== "string" || !updatedModel.uri.trim()) {
980
+ throw new Error("ADAPTER_OUTPUT_INVALID: updatedModel.uri must be a non-empty string.");
981
+ }
982
+ if (!Number.isFinite(value.changedTensorCount) || value.changedTensorCount < 0) {
983
+ throw new Error("ADAPTER_OUTPUT_INVALID: changedTensorCount must be a finite number >= 0.");
984
+ }
985
+ if (typeof value.changedParamCount !== "undefined" && (!Number.isFinite(value.changedParamCount) || value.changedParamCount < 0)) {
986
+ throw new Error("ADAPTER_OUTPUT_INVALID: changedParamCount must be a finite number >= 0 when provided.");
987
+ }
988
+ if (typeof value.metrics !== "undefined" && !isNonArrayRecord(value.metrics)) {
989
+ throw new Error("ADAPTER_OUTPUT_INVALID: metrics must be an object when provided.");
990
+ }
991
+ if (typeof value.artifacts !== "undefined") {
992
+ if (!Array.isArray(value.artifacts)) {
993
+ throw new Error("ADAPTER_OUTPUT_INVALID: artifacts must be an array when provided.");
994
+ }
995
+ for (const artifact of value.artifacts) {
996
+ if (!artifact || typeof artifact.name !== "string" || typeof artifact.uri !== "string") {
997
+ throw new Error("ADAPTER_OUTPUT_INVALID: each artifact requires string name and uri.");
998
+ }
999
+ }
1000
+ }
1001
+ };
1002
+ var assertEvaluationOutput = (value) => {
1003
+ if (!value || !["PASS", "FAIL", "REVIEW"].includes(value.finalVerdict)) {
1004
+ throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.finalVerdict must be PASS | FAIL | REVIEW.");
1005
+ }
1006
+ if (!Number.isFinite(value.leakScore) || value.leakScore < 0) {
1007
+ throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.leakScore must be a finite number >= 0.");
1008
+ }
1009
+ if (typeof value.details !== "undefined" && !isNonArrayRecord(value.details)) {
1010
+ throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.details must be an object when provided.");
1011
+ }
1012
+ };
1013
+ var sha256Hex = (value) => {
1014
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
1015
+ };
1016
+ var buildResultClaim = (claim, mode) => {
1017
+ if (mode === "raw") {
1018
+ return claim;
1019
+ }
1020
+ const base = {
1021
+ claim_type: claim.claim_type,
1022
+ scope: claim.scope,
1023
+ assertion: claim.assertion
1024
+ };
1025
+ if (mode === "omit") {
1026
+ return base;
1027
+ }
1028
+ const payload = typeof claim.claim_payload === "string" ? claim.claim_payload : "";
1029
+ return {
1030
+ ...base,
1031
+ payload_hash: sha256Hex(payload),
1032
+ claim_payload_sha256: sha256Hex(payload),
1033
+ claim_payload_length: payload.length
1034
+ };
1035
+ };
1036
+ var WhiteboxWorker = class {
1037
+ constructor(client, adapter, options) {
1038
+ this.client = client;
1039
+ this.adapter = adapter;
1040
+ this.options = options;
1041
+ this.running = false;
1042
+ this.loopPromise = null;
1043
+ this.pollIntervalMs = options.pollIntervalMs ?? 2e3;
1044
+ this.claimBatchSize = options.claimBatchSize ?? 1;
1045
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 2e4;
1046
+ this.supportedJobTypes = options.supportedJobTypes ?? ["MODEL_UNLEARN"];
1047
+ this.qualityGatePolicy = {
1048
+ ...DEFAULT_WHITEBOX_QUALITY_GATE_POLICY,
1049
+ ...options.qualityGatePolicy || {}
1050
+ };
1051
+ this.qualityGateOnFailure = options.qualityGateOnFailure || "fail_job";
1052
+ this.resultClaimMode = options.resultClaimMode || "hash";
1053
+ this.logger = options.logger ?? defaultLogger;
1054
+ }
1055
+ async start() {
1056
+ if (this.running) {
1057
+ return;
1058
+ }
1059
+ this.running = true;
1060
+ this.logger.info("worker started", {
1061
+ projectId: this.options.projectId,
1062
+ adapter: this.adapter.name,
1063
+ claimBatchSize: this.claimBatchSize
1064
+ });
1065
+ this.loopPromise = this.runLoop();
1066
+ }
1067
+ async stop() {
1068
+ this.running = false;
1069
+ if (this.loopPromise) {
1070
+ await this.loopPromise;
1071
+ this.loopPromise = null;
1072
+ }
1073
+ this.logger.info("worker stopped", { projectId: this.options.projectId });
1074
+ }
1075
+ async runOnce() {
1076
+ const jobs = await this.client.claimJobs(this.options.projectId, {
1077
+ limit: this.claimBatchSize,
1078
+ types: this.supportedJobTypes
1079
+ });
1080
+ if (!jobs.length) {
1081
+ return;
1082
+ }
1083
+ for (const job of jobs) {
1084
+ await this.processClaimedJob(job);
1085
+ }
1086
+ }
1087
+ async runLoop() {
1088
+ while (this.running) {
1089
+ try {
1090
+ await this.runOnce();
1091
+ } catch (error) {
1092
+ this.logger.error("worker loop error", {
1093
+ projectId: this.options.projectId,
1094
+ error: safeErrorMessage(error)
1095
+ });
1096
+ }
1097
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1098
+ }
1099
+ }
1100
+ async processClaimedJob(job) {
1101
+ const claimId = typeof job.lastClaimId === "string" ? job.lastClaimId : void 0;
1102
+ if (!this.supportedJobTypes.includes(job.type)) {
1103
+ this.logger.warn("unsupported job type claimed", {
1104
+ jobId: job.id,
1105
+ type: job.type
1106
+ });
1107
+ await this.client.failJob(this.options.projectId, job.id, {
1108
+ error: {
1109
+ code: "VALIDATION_ERROR",
1110
+ message: `Unsupported job type: ${job.type}`
1111
+ }
1112
+ }, { claimId });
1113
+ return;
1114
+ }
1115
+ const context = {
1116
+ projectId: this.options.projectId,
1117
+ jobId: job.id,
1118
+ correlationId: claimId
1119
+ };
1120
+ let heartbeatTimer = null;
1121
+ const heartbeat = async () => {
1122
+ try {
1123
+ await this.client.heartbeatJob(this.options.projectId, job.id, { claimId });
1124
+ } catch (error) {
1125
+ this.logger.warn("heartbeat failed", {
1126
+ jobId: job.id,
1127
+ error: safeErrorMessage(error)
1128
+ });
1129
+ }
1130
+ };
1131
+ try {
1132
+ const claim = toClaim(job);
1133
+ const config = toConfig(job);
1134
+ const model = resolveModel(config);
1135
+ const target = resolveTarget(claim, config);
1136
+ const plan = resolvePlan(config);
1137
+ const perJobQualityGate = resolvePerJobQualityGatePolicy(this.qualityGatePolicy, config, plan);
1138
+ heartbeatTimer = setInterval(heartbeat, this.heartbeatIntervalMs);
1139
+ await heartbeat();
1140
+ const unlearningOutput = await this.adapter.runUnlearning(
1141
+ { model, target, plan, claim, config },
1142
+ context
1143
+ );
1144
+ assertUnlearningOutput(unlearningOutput);
1145
+ let evaluationOutput;
1146
+ if (this.adapter.runEvaluation) {
1147
+ evaluationOutput = await this.adapter.runEvaluation(
1148
+ {
1149
+ model: unlearningOutput.updatedModel,
1150
+ baselineModel: model,
1151
+ claim,
1152
+ config,
1153
+ probes: config.parameters?.probes || []
1154
+ },
1155
+ context
1156
+ );
1157
+ assertEvaluationOutput(evaluationOutput);
1158
+ }
1159
+ const qualityGate = evaluateWhiteboxQualityGate(
1160
+ unlearningOutput,
1161
+ evaluationOutput,
1162
+ perJobQualityGate.policy
1163
+ );
1164
+ if (!qualityGate.pass && this.qualityGateOnFailure === "fail_job") {
1165
+ throw new Error(`QUALITY_GATE_FAILED: ${qualityGate.reasons.join(" | ")}`);
1166
+ }
1167
+ const finalVerdict = qualityGate.pass ? evaluationOutput?.finalVerdict || "REVIEW" : "REVIEW";
1168
+ const result = {
1169
+ result: {
1170
+ worker: {
1171
+ adapter: this.adapter.name,
1172
+ mode: "whitebox"
1173
+ },
1174
+ claim: buildResultClaim(claim, this.resultClaimMode),
1175
+ plan,
1176
+ unlearning: unlearningOutput,
1177
+ evaluation: evaluationOutput || null,
1178
+ quality_gate: qualityGate,
1179
+ quality_gate_preset: perJobQualityGate.preset,
1180
+ final_verdict: finalVerdict,
1181
+ report_hash: unlearningOutput.modelAfterSha256 || null,
1182
+ privacy: {
1183
+ claim_mode: this.resultClaimMode
1184
+ }
1185
+ }
1186
+ };
1187
+ await this.client.completeJob(this.options.projectId, job.id, result, { claimId });
1188
+ this.logger.info("job completed", {
1189
+ jobId: job.id,
1190
+ type: job.type,
1191
+ adapter: this.adapter.name
1192
+ });
1193
+ } catch (error) {
1194
+ this.logger.error("job failed", {
1195
+ jobId: job.id,
1196
+ type: job.type,
1197
+ error: safeErrorMessage(error)
1198
+ });
1199
+ await this.client.failJob(this.options.projectId, job.id, toJobError(error), { claimId });
1200
+ } finally {
1201
+ if (heartbeatTimer) {
1202
+ clearInterval(heartbeatTimer);
1203
+ }
1204
+ }
1205
+ }
1206
+ };
1207
+
1208
+ // src/native-adapters.ts
1209
+ import { spawn } from "child_process";
1210
+ var DEFAULT_ADAPTER_TIMEOUT_MS = 30 * 60 * 1e3;
1211
+ var LocalCommandTrainingAdapter = class {
1212
+ constructor(options) {
1213
+ this.options = options;
1214
+ this.name = options.name || "local-command-adapter";
1215
+ }
1216
+ async runUnlearning(input, context) {
1217
+ const spec = resolveCommandSpec(this.options.unlearning, input, context);
1218
+ const output = await runJsonCommand(
1219
+ spec,
1220
+ {
1221
+ mode: "unlearning",
1222
+ input,
1223
+ context
1224
+ },
1225
+ this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
1226
+ );
1227
+ return output;
1228
+ }
1229
+ async runEvaluation(input, context) {
1230
+ if (!this.options.evaluation) {
1231
+ throw new Error("LocalCommandTrainingAdapter evaluation command is not configured.");
1232
+ }
1233
+ const spec = resolveCommandSpec(this.options.evaluation, input, context);
1234
+ const output = await runJsonCommand(
1235
+ spec,
1236
+ {
1237
+ mode: "evaluation",
1238
+ input,
1239
+ context
1240
+ },
1241
+ this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
1242
+ );
1243
+ return output;
1244
+ }
1245
+ };
1246
+ var HttpTrainingAdapter = class {
1247
+ constructor(options) {
1248
+ this.options = options;
1249
+ this.name = options.name || "http-training-adapter";
1250
+ }
1251
+ async runUnlearning(input, context) {
1252
+ const output = await postJson(
1253
+ this.options.unlearningUrl,
1254
+ {
1255
+ mode: "unlearning",
1256
+ input,
1257
+ context
1258
+ },
1259
+ this.options.headers,
1260
+ this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
1261
+ );
1262
+ return output;
1263
+ }
1264
+ async runEvaluation(input, context) {
1265
+ if (!this.options.evaluationUrl) {
1266
+ throw new Error("HttpTrainingAdapter evaluationUrl is not configured.");
1267
+ }
1268
+ const output = await postJson(
1269
+ this.options.evaluationUrl,
1270
+ {
1271
+ mode: "evaluation",
1272
+ input,
1273
+ context
1274
+ },
1275
+ this.options.headers,
1276
+ this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
1277
+ );
1278
+ return output;
1279
+ }
1280
+ };
1281
+ var createNativeTrainingAdapter = (config) => {
1282
+ if (config.provider === "local_command") {
1283
+ const { provider: _provider2, ...rest2 } = config;
1284
+ return new LocalCommandTrainingAdapter(rest2);
1285
+ }
1286
+ const { provider: _provider, ...rest } = config;
1287
+ return new HttpTrainingAdapter(rest);
1288
+ };
1289
+ function resolveCommandSpec(resolver, input, context) {
1290
+ const resolved = typeof resolver === "function" ? resolver(input, context) : resolver;
1291
+ if (!resolved?.command) {
1292
+ throw new Error("Adapter command is missing.");
1293
+ }
1294
+ return resolved;
1295
+ }
1296
+ async function runJsonCommand(spec, payload, defaultTimeoutMs) {
1297
+ const timeoutMs = spec.timeoutMs ?? defaultTimeoutMs;
1298
+ return new Promise((resolve, reject) => {
1299
+ const child = spawn(spec.command, spec.args || [], {
1300
+ cwd: spec.cwd,
1301
+ env: { ...process.env, ...spec.env || {} },
1302
+ stdio: ["pipe", "pipe", "pipe"]
1303
+ });
1304
+ let stdout = "";
1305
+ let stderr = "";
1306
+ let timedOut = false;
1307
+ let hardKillTimer = null;
1308
+ const timer = setTimeout(() => {
1309
+ timedOut = true;
1310
+ child.kill("SIGTERM");
1311
+ hardKillTimer = setTimeout(() => child.kill("SIGKILL"), 1e3);
1312
+ }, timeoutMs);
1313
+ child.stdout.on("data", (chunk) => {
1314
+ stdout += chunk.toString();
1315
+ });
1316
+ child.stderr.on("data", (chunk) => {
1317
+ stderr += chunk.toString();
1318
+ });
1319
+ child.on("error", (error) => {
1320
+ clearTimeout(timer);
1321
+ if (hardKillTimer) clearTimeout(hardKillTimer);
1322
+ reject(error);
1323
+ });
1324
+ child.on("close", (code) => {
1325
+ clearTimeout(timer);
1326
+ if (hardKillTimer) clearTimeout(hardKillTimer);
1327
+ if (timedOut) {
1328
+ reject(new Error(`Adapter command timed out after ${timeoutMs}ms: ${spec.command}`));
1329
+ return;
1330
+ }
1331
+ if (code !== 0) {
1332
+ reject(
1333
+ new Error(
1334
+ `Adapter command failed (${code}): ${spec.command} ${(spec.args || []).join(" ")}; stderr=${stderr.trim()}`
1335
+ )
1336
+ );
1337
+ return;
1338
+ }
1339
+ const trimmed = stdout.trim();
1340
+ if (!trimmed) {
1341
+ reject(new Error(`Adapter command returned empty stdout: ${spec.command}`));
1342
+ return;
1343
+ }
1344
+ try {
1345
+ resolve(JSON.parse(trimmed));
1346
+ } catch {
1347
+ reject(new Error(`Adapter command output is not valid JSON: ${trimmed.slice(0, 800)}`));
1348
+ }
1349
+ });
1350
+ if (spec.stdinMode !== "none") {
1351
+ try {
1352
+ child.stdin.write(JSON.stringify(payload));
1353
+ } catch (error) {
1354
+ clearTimeout(timer);
1355
+ if (hardKillTimer) clearTimeout(hardKillTimer);
1356
+ reject(error);
1357
+ return;
1358
+ }
1359
+ }
1360
+ child.stdin.end();
1361
+ });
1362
+ }
1363
+ async function postJson(url, body, headers, timeoutMs) {
1364
+ const controller = new AbortController();
1365
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1366
+ try {
1367
+ const response = await fetch(url, {
1368
+ method: "POST",
1369
+ headers: {
1370
+ "content-type": "application/json",
1371
+ ...headers || {}
1372
+ },
1373
+ body: JSON.stringify(body),
1374
+ signal: controller.signal
1375
+ });
1376
+ const text = await response.text();
1377
+ if (!response.ok) {
1378
+ throw new Error(`HTTP adapter request failed (${response.status}): ${text.slice(0, 800)}`);
1379
+ }
1380
+ if (!text.trim()) {
1381
+ throw new Error(`HTTP adapter returned empty response: ${url}`);
1382
+ }
1383
+ try {
1384
+ return JSON.parse(text);
1385
+ } catch {
1386
+ throw new Error(`HTTP adapter returned non-JSON response: ${text.slice(0, 800)}`);
1387
+ }
1388
+ } finally {
1389
+ clearTimeout(timeout);
1390
+ }
1391
+ }
1392
+
1393
+ // src/deployment-profiles.ts
1394
+ var ENTERPRISE_STRICT_QUALITY_GATE_POLICY = {
1395
+ ...WHITEBOX_QUALITY_GATE_PRESETS.strict
1396
+ };
1397
+ function assertDeploymentCompatibility(options) {
1398
+ const normalizedHost = parseHost(options.apiUrl);
1399
+ if (!normalizedHost) {
1400
+ throw new Error(`Invalid FORG3T_API_URL: ${options.apiUrl}`);
1401
+ }
1402
+ if (options.profile !== "customer_airgapped") {
1403
+ return;
1404
+ }
1405
+ if (options.adapterMode === "http" && !options.allowHttpInAirgapped) {
1406
+ throw new Error(
1407
+ "Air-gapped profile rejects adapterMode=http by default. Use local_command or explicitly set allowHttpInAirgapped=true for trusted internal gateways."
1408
+ );
1409
+ }
1410
+ if (!isHostAllowedForAirgapped(normalizedHost, options.allowedApiHosts || [])) {
1411
+ throw new Error(
1412
+ `Air-gapped profile requires a private/control-plane host. Received ${normalizedHost}. Set allowedApiHosts for private DNS hosts if needed.`
1413
+ );
1414
+ }
1415
+ }
1416
+ function buildWhiteboxWorkerOptions(profile, baseOptions) {
1417
+ const strictMode = profile === "customer_airgapped" || profile === "customer_online";
1418
+ const defaultPolicy = strictMode ? ENTERPRISE_STRICT_QUALITY_GATE_POLICY : DEFAULT_WHITEBOX_QUALITY_GATE_POLICY;
1419
+ const defaultSupportedTypes = ["MODEL_UNLEARN"];
1420
+ return {
1421
+ projectId: baseOptions.projectId,
1422
+ pollIntervalMs: baseOptions.pollIntervalMs ?? 2e3,
1423
+ claimBatchSize: baseOptions.claimBatchSize ?? 1,
1424
+ heartbeatIntervalMs: baseOptions.heartbeatIntervalMs ?? 2e4,
1425
+ supportedJobTypes: baseOptions.supportedJobTypes ?? defaultSupportedTypes,
1426
+ qualityGatePolicy: baseOptions.qualityGatePolicy ?? defaultPolicy,
1427
+ qualityGateOnFailure: baseOptions.qualityGateOnFailure ?? "fail_job",
1428
+ resultClaimMode: baseOptions.resultClaimMode ?? "hash",
1429
+ logger: baseOptions.logger
1430
+ };
1431
+ }
1432
+ function parseHost(apiUrl) {
1433
+ try {
1434
+ const parsed = new URL(apiUrl);
1435
+ return parsed.hostname.toLowerCase();
1436
+ } catch {
1437
+ return null;
1438
+ }
1439
+ }
1440
+ function isHostAllowedForAirgapped(host, allowedApiHosts) {
1441
+ if (isPrivateHost(host)) {
1442
+ return true;
1443
+ }
1444
+ const allowSet = new Set(allowedApiHosts.map((item) => item.trim().toLowerCase()).filter(Boolean));
1445
+ return allowSet.has(host);
1446
+ }
1447
+ function isPrivateHost(host) {
1448
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
1449
+ return true;
1450
+ }
1451
+ if (host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan")) {
1452
+ return true;
1453
+ }
1454
+ const parts = host.split(".");
1455
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(Number(part)))) {
1456
+ return false;
1457
+ }
1458
+ const octets = parts.map((part) => Number(part));
1459
+ const [a, b] = octets;
1460
+ if (a === 10) return true;
1461
+ if (a === 127) return true;
1462
+ if (a === 192 && b === 168) return true;
1463
+ if (a === 172 && b >= 16 && b <= 31) return true;
1464
+ return false;
1465
+ }
424
1466
  export {
1467
+ AccessLevel,
1468
+ DEFAULT_WHITEBOX_QUALITY_GATE_POLICY,
1469
+ ENTERPRISE_STRICT_QUALITY_GATE_POLICY,
425
1470
  Forg3tApiConnectionError,
426
1471
  Forg3tAuthenticationError,
427
1472
  Forg3tClient,
428
1473
  Forg3tError,
429
1474
  Forg3tNotFoundError,
430
- Forg3tRateLimitError
1475
+ Forg3tNotImplementedError,
1476
+ Forg3tRateLimitError,
1477
+ HttpTrainingAdapter,
1478
+ IntegrationMode,
1479
+ IntegrationType,
1480
+ LocalCommandTrainingAdapter,
1481
+ WHITEBOX_QUALITY_GATE_PRESETS,
1482
+ WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS,
1483
+ WhiteboxWorker,
1484
+ assertDeploymentCompatibility,
1485
+ buildModelUnlearningPayload,
1486
+ buildWhiteboxWorkerOptions,
1487
+ createNativeTrainingAdapter,
1488
+ evaluateWhiteboxQualityGate,
1489
+ resolveWhiteboxQualityGatePolicy
431
1490
  };