witnora 0.18.2 → 0.18.4
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/gateway-service.js +8 -2
- package/dist/gateway.js +66 -0
- package/dist/onboard.js +4 -1
- package/dist/private-discovery.js +17 -8
- package/dist/real-path-activation.js +12 -2
- package/package.json +1 -1
package/dist/gateway-service.js
CHANGED
|
@@ -18,7 +18,13 @@ export async function installGatewayService(input) {
|
|
|
18
18
|
export async function installCurrentGatewayService(options) {
|
|
19
19
|
const planInput = currentPlanInput(options);
|
|
20
20
|
return installGatewayService({ ...planInput,
|
|
21
|
-
writeDefinition: async (path, value) => {
|
|
21
|
+
writeDefinition: async (path, value) => {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
const contents = planInput.platform === "win32"
|
|
24
|
+
? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(value, "utf16le")])
|
|
25
|
+
: Buffer.from(value, "utf8");
|
|
26
|
+
await writeFile(path, contents, { mode: 0o600 });
|
|
27
|
+
},
|
|
22
28
|
run: options.run ?? runCommand,
|
|
23
29
|
});
|
|
24
30
|
}
|
|
@@ -53,7 +59,7 @@ export function createGatewayServicePlan(input) {
|
|
|
53
59
|
function windowsPlan(input, id, args) {
|
|
54
60
|
const definitionPath = `${input.serviceHome}\\${id}.xml`;
|
|
55
61
|
const argumentsValue = args.map(windowsArgument).join(" ");
|
|
56
|
-
const definition = `<?xml version="1.0" encoding="UTF-
|
|
62
|
+
const definition = `<?xml version="1.0" encoding="UTF-16"?>
|
|
57
63
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
58
64
|
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(input.userId)}</UserId></LogonTrigger></Triggers>
|
|
59
65
|
<Principals><Principal id="Author"><UserId>${xml(input.userId)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
package/dist/gateway.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, createPrivateKey, createPublicKey, randomBytes } from "node
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { closeSync, openSync } from "node:fs";
|
|
4
4
|
import { access, chmod, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createServer as createNetServer } from "node:net";
|
|
5
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
import { loadConnection } from "./credentials.js";
|
|
@@ -196,6 +197,44 @@ export async function upgradeCustomerGatewayRuntime(options) {
|
|
|
196
197
|
}
|
|
197
198
|
return { config: next, generatedFiles: [configPath, clientPath, readmePath, ...created, ...(replacingGeneratedRuntime ? [join(directory, runtimeKit.config.adapterModulePath), join(directory, runtimeKit.config.probeModulePath)] : [])], changed: true, rollback: restore };
|
|
198
199
|
}
|
|
200
|
+
export async function ensureCustomerGatewayPortAvailable(options) {
|
|
201
|
+
const directory = resolve(options.repository, options.dir ?? ".witnora/gateway");
|
|
202
|
+
const configPath = join(directory, "gateway.json");
|
|
203
|
+
const clientPath = join(directory, "client.mjs");
|
|
204
|
+
const readmePath = join(directory, "README.md");
|
|
205
|
+
const [configRaw, clientRaw, readmeRaw] = await Promise.all([
|
|
206
|
+
readFile(configPath, "utf8"), readFile(clientPath, "utf8"), readFile(readmePath, "utf8"),
|
|
207
|
+
]);
|
|
208
|
+
const current = parseConfig(configRaw);
|
|
209
|
+
const health = await localCollector(current.host, current.port, options.fetch ?? fetch);
|
|
210
|
+
if (health === current.collectorId || (!health && await canListen(current.host, current.port))) {
|
|
211
|
+
return { changed: false, port: current.port };
|
|
212
|
+
}
|
|
213
|
+
if (clientRaw !== gatewayClient(current) || readmeRaw !== gatewayReadme(current)) {
|
|
214
|
+
throw new Error(`Gateway port ${current.port} is occupied, and generated Gateway files were modified; refusing to rewrite customer files.`);
|
|
215
|
+
}
|
|
216
|
+
const excluded = new Set([current.port]);
|
|
217
|
+
if (current.runtimeWorker?.sandboxOrigin)
|
|
218
|
+
excluded.add(Number(new URL(current.runtimeWorker.sandboxOrigin).port));
|
|
219
|
+
const port = await nextAvailablePort(current.host, current.port, excluded);
|
|
220
|
+
const next = { ...current, port };
|
|
221
|
+
if (next.runtimeWorker)
|
|
222
|
+
next.runtimeWorker = { ...next.runtimeWorker, configDigestSha256: undefined };
|
|
223
|
+
if (next.runtimeWorker)
|
|
224
|
+
next.runtimeWorker.configDigestSha256 = gatewayConfigDigest(next);
|
|
225
|
+
try {
|
|
226
|
+
await atomicWrite(configPath, `${JSON.stringify(next, null, 2)}\n`, 0o644);
|
|
227
|
+
await atomicWrite(clientPath, gatewayClient(next), 0o644);
|
|
228
|
+
await atomicWrite(readmePath, gatewayReadme(next), 0o644);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
await Promise.all([
|
|
232
|
+
atomicWrite(configPath, configRaw, 0o644), atomicWrite(clientPath, clientRaw, 0o644), atomicWrite(readmePath, readmeRaw, 0o644),
|
|
233
|
+
]).catch(() => undefined);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
return { changed: true, port };
|
|
237
|
+
}
|
|
199
238
|
function sameRuntimeGeneration(left, right) {
|
|
200
239
|
return JSON.stringify({ ...left, configDigestSha256: undefined }) === JSON.stringify({ ...right, configDigestSha256: undefined });
|
|
201
240
|
}
|
|
@@ -1412,6 +1451,33 @@ async function atomicWrite(path, content, mode) {
|
|
|
1412
1451
|
await rename(temporary, path);
|
|
1413
1452
|
await chmod(path, mode).catch(() => undefined);
|
|
1414
1453
|
}
|
|
1454
|
+
async function localCollector(host, port, requestFetch) {
|
|
1455
|
+
try {
|
|
1456
|
+
const response = await requestFetch(`http://${host}:${port}/healthz`, { signal: AbortSignal.timeout(750) });
|
|
1457
|
+
if (!response.ok)
|
|
1458
|
+
return "occupied";
|
|
1459
|
+
const value = await response.json();
|
|
1460
|
+
return typeof value.collectorId === "string" ? value.collectorId : "occupied";
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
return undefined;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function canListen(host, port) {
|
|
1467
|
+
const server = createNetServer();
|
|
1468
|
+
return new Promise((resolvePromise) => {
|
|
1469
|
+
server.once("error", () => resolvePromise(false));
|
|
1470
|
+
server.listen(port, host, () => server.close(() => resolvePromise(true)));
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
async function nextAvailablePort(host, preferred, excluded) {
|
|
1474
|
+
for (let offset = 1; offset <= 1_000; offset += 1) {
|
|
1475
|
+
const candidate = 1_024 + ((preferred - 1_024 + offset) % (65_535 - 1_024));
|
|
1476
|
+
if (!excluded.has(candidate) && await canListen(host, candidate))
|
|
1477
|
+
return candidate;
|
|
1478
|
+
}
|
|
1479
|
+
throw new Error("No available localhost port was found for the customer-owned Gateway.");
|
|
1480
|
+
}
|
|
1415
1481
|
async function exists(path) {
|
|
1416
1482
|
try {
|
|
1417
1483
|
await access(path);
|
package/dist/onboard.js
CHANGED
|
@@ -8,7 +8,7 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
|
8
8
|
import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
9
9
|
import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
|
|
10
10
|
import { writeTryEvidence } from "./try.js";
|
|
11
|
-
import { doctorCustomerGateway, activateManagedWorkflowHarness, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
|
|
11
|
+
import { doctorCustomerGateway, activateManagedWorkflowHarness, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
|
|
12
12
|
import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
|
|
13
13
|
import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
|
|
14
14
|
import { activateRealPathIntegrations } from "./real-path-activation.js";
|
|
@@ -217,6 +217,9 @@ export async function runOnboard(options) {
|
|
|
217
217
|
throw new Error("The prior managed Gateway process did not stop before Assurance Harness activation.");
|
|
218
218
|
}
|
|
219
219
|
if (!options.gatewayLifecycle) {
|
|
220
|
+
const port = await ensureCustomerGatewayPortAvailable({ repository: repositoryPath });
|
|
221
|
+
if (port.changed)
|
|
222
|
+
output(`Rebound this repository's generated Gateway to available localhost port ${port.port}; another project remains untouched.\n`);
|
|
220
223
|
const installed = await installCurrentGatewayService({ repository: repositoryPath, cliEntry: fileURLToPath(new URL("./cli.js", import.meta.url)) });
|
|
221
224
|
continuousService = { state: "INSTALLED", kind: installed.plan.kind, id: installed.plan.id };
|
|
222
225
|
}
|
|
@@ -31,7 +31,7 @@ export async function runPrivateCapabilityDiscovery(input) {
|
|
|
31
31
|
localRedactionApplied: true,
|
|
32
32
|
},
|
|
33
33
|
capabilities: input.repository.capabilities,
|
|
34
|
-
unknownCapabilityCount: input.repository.capabilities.length,
|
|
34
|
+
unknownCapabilityCount: input.repository.capabilities.filter((item) => item.disposition === "pending").length,
|
|
35
35
|
};
|
|
36
36
|
const payloadSha256 = sha256(canonicalJson(payload));
|
|
37
37
|
const privateKey = createPrivateKey(identity.privateKeyPem);
|
|
@@ -62,12 +62,13 @@ export async function runPrivateCapabilityDiscovery(input) {
|
|
|
62
62
|
export function inferPrivateCapabilities(input) {
|
|
63
63
|
const searchable = [...input.dependencyNames, ...input.topLevelNames].join(" ").toLowerCase();
|
|
64
64
|
const detected = new Map();
|
|
65
|
-
const add = (key, observedName, transport) => detected.set(key, {
|
|
65
|
+
const add = (key, observedName, transport, capabilityId, disposition = "pending") => detected.set(key, {
|
|
66
66
|
key,
|
|
67
67
|
observedName,
|
|
68
68
|
transport,
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
...(capabilityId ? { capabilityId } : {}),
|
|
70
|
+
disposition,
|
|
71
|
+
schemaSha256: sha256(canonicalJson({ key, observedName, transport, capabilityId, disposition, source: "repository_metadata" })),
|
|
71
72
|
source: "repository_metadata",
|
|
72
73
|
});
|
|
73
74
|
add("workflow:orchestration", "Workflow orchestration", "workflow");
|
|
@@ -84,7 +85,7 @@ export function inferPrivateCapabilities(input) {
|
|
|
84
85
|
if (/slack|discord|twilio|resend|sendgrid|postmark|nodemailer|email/.test(searchable))
|
|
85
86
|
add("messaging:external", "External messaging", "messaging");
|
|
86
87
|
for (const capability of repositoryManifestCapabilities(input.manifest))
|
|
87
|
-
add(capability.key, capability.observedName, capability.transport);
|
|
88
|
+
add(capability.key, capability.observedName, capability.transport, capability.capabilityId, capability.disposition);
|
|
88
89
|
return [...detected.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
89
90
|
}
|
|
90
91
|
function repositoryManifestCapabilities(value) {
|
|
@@ -101,13 +102,21 @@ function repositoryManifestCapabilities(value) {
|
|
|
101
102
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
102
103
|
throw new Error(`Repository discovery manifest capability ${index} is invalid.`);
|
|
103
104
|
const capability = item;
|
|
104
|
-
if (Object.keys(capability).some((key) =>
|
|
105
|
+
if (Object.keys(capability).some((key) => !["key", "observedName", "transport", "capabilityId", "disposition"].includes(key))
|
|
105
106
|
|| typeof capability.key !== "string" || !/^[A-Za-z0-9._:-]{1,120}$/.test(capability.key)
|
|
106
107
|
|| typeof capability.observedName !== "string" || !/^[A-Za-z0-9._:-]{1,120}$/.test(capability.observedName)
|
|
107
|
-
|| !transports.has(capability.transport)
|
|
108
|
+
|| !transports.has(capability.transport)
|
|
109
|
+
|| capability.capabilityId !== undefined && (typeof capability.capabilityId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(capability.capabilityId))
|
|
110
|
+
|| capability.disposition !== undefined && !["allowed", "approval_required", "denied", "pending"].includes(String(capability.disposition))) {
|
|
108
111
|
throw new Error(`Repository discovery manifest capability ${index} failed its bounded contract.`);
|
|
109
112
|
}
|
|
110
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
key: capability.key,
|
|
115
|
+
observedName: capability.observedName,
|
|
116
|
+
transport: capability.transport,
|
|
117
|
+
...(capability.capabilityId ? { capabilityId: capability.capabilityId } : {}),
|
|
118
|
+
disposition: capability.disposition ?? "pending",
|
|
119
|
+
};
|
|
111
120
|
});
|
|
112
121
|
}
|
|
113
122
|
function discoveryIdentityPath(projectId, configHome) {
|
|
@@ -84,6 +84,8 @@ async function providerPreflight(request, env, plan, now, postgresClientFactory)
|
|
|
84
84
|
throw new Error(`${plan.generated.providerPackId} activation is not implemented by this CLI version.`);
|
|
85
85
|
}
|
|
86
86
|
async function shopifyDisputePreflight(request, env, plan, now) {
|
|
87
|
+
if (plan.environment === "sandbox" && plan.generated.providerConfiguration === undefined)
|
|
88
|
+
return shopifySandboxFixturePreflight(plan, now);
|
|
87
89
|
const token = env.SHOPIFY_READ_ACCESS_TOKEN;
|
|
88
90
|
if (!token || token.length < 8)
|
|
89
91
|
throw new Error("Shopify activation requires the read-only SHOPIFY_READ_ACCESS_TOKEN in the customer environment.");
|
|
@@ -103,6 +105,12 @@ async function shopifyDisputePreflight(request, env, plan, now) {
|
|
|
103
105
|
assertCriterion(plan, observation);
|
|
104
106
|
return preflightResult(plan, now, "shopify", id, observation);
|
|
105
107
|
}
|
|
108
|
+
function shopifySandboxFixturePreflight(plan, now) {
|
|
109
|
+
const resourceId = `sellershield-dispute-${sha(plan.taskContractDigestSha256).slice(0, 16)}`;
|
|
110
|
+
const observation = { status: plan.generated.criterion.expected };
|
|
111
|
+
const resultDigest = sha(canonical(observation));
|
|
112
|
+
return { acceptance: { kind: "READ_ONLY_PROVIDER_PREFLIGHT", passedAt: now.toISOString(), productionWrites: 0, observationDigestSha256: resultDigest }, scenario: { id: `shopify-sandbox:${sha(resourceId).slice(0, 16)}`, source: "SANDBOX_FIXTURE", sanitized: true, input: { resourceId }, inputDigestSha256: sha(canonical({ resourceId })), baseline: { resultDigestSha256: resultDigest, actionIntents: plan.generated.actionPathBindings.map((item) => ({ pathId: item.actionPathId, parametersDigestSha256: sha(resourceId) })) } } };
|
|
113
|
+
}
|
|
106
114
|
async function stripePreflight(request, secret, plan, now) {
|
|
107
115
|
const response = await request("https://api.stripe.com/v1/refunds?limit=1", { headers: { authorization: `Bearer ${secret}` }, redirect: "error", signal: AbortSignal.timeout(5_000) });
|
|
108
116
|
if (!response.ok)
|
|
@@ -214,14 +222,14 @@ async function postgresPreflight(env, plan, now, clientFactory) {
|
|
|
214
222
|
}
|
|
215
223
|
async function defaultPostgresClient(connectionString) { const imported = await import("pg"); return new imported.Client({ connectionString, application_name: "witnora-read-only-preflight" }); }
|
|
216
224
|
function generatedProviderHarness(plans) {
|
|
217
|
-
const contracts = plans.map((plan) => ({ taskContractId: plan.taskContractId, providerPackId: plan.generated.providerPackId, providerConfiguration: plan.generated.providerConfiguration ?? {}, credentialHandle: `.witnora/provider-credentials/${plan.id}.secret`, criterion: plan.generated.criterion, actionPathIds: plan.generated.actionPathBindings.map((item) => item.actionPathId) }));
|
|
225
|
+
const contracts = plans.map((plan) => ({ taskContractId: plan.taskContractId, providerPackId: plan.generated.providerPackId, providerConfiguration: plan.generated.providerConfiguration ?? {}, credentialHandle: `.witnora/provider-credentials/${plan.id}.secret`, criterion: plan.generated.criterion, actionPathIds: plan.generated.actionPathBindings.map((item) => item.actionPathId), ...(plan.generated.providerPackId === "SHOPIFY_DISPUTE" && plan.environment === "sandbox" && plan.generated.providerConfiguration === undefined ? { sandboxObservation: { [plan.generated.criterion.field]: plan.generated.criterion.expected } } : {}) }));
|
|
218
226
|
return `import {createHash} from "node:crypto";
|
|
219
227
|
import {readFile} from "node:fs/promises";
|
|
220
228
|
import {join} from "node:path";
|
|
221
229
|
const contracts=${JSON.stringify(contracts)};
|
|
222
230
|
const sha=(value)=>createHash("sha256").update(typeof value==="string"?value:JSON.stringify(value)).digest("hex");
|
|
223
231
|
const request=async(url,key)=>{const response=await fetch(url,{method:"GET",headers:{accept:"application/json",authorization:"Bearer "+key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return response.json();};
|
|
224
|
-
const observe=async(contract,resourceId,repository)=>{const key=(await readFile(join(repository,contract.credentialHandle),"utf8")).trim();if(!key)throw new Error("The local read-only Provider credential is unavailable.");if(contract.providerPackId==="STRIPE_REFUND"){if(!key.startsWith("sk_test_"))throw new Error("Stripe test-mode credential is unavailable.");return request("https://api.stripe.com/v1/refunds/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="SHOPIFY_DISPUTE"){const response=await fetch(contract.providerConfiguration.origin+"/admin/api/2025-07/shopify_payments/disputes/"+encodeURIComponent(resourceId)+".json",{method:"GET",headers:{accept:"application/json","x-shopify-access-token":key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return (await response.json()).dispute;}if(contract.providerPackId==="ZENDESK_TICKET")return (await request(contract.providerConfiguration.origin+"/api/v2/tickets/"+encodeURIComponent(resourceId)+".json",key)).ticket;if(contract.providerPackId==="SALESFORCE_RECORD")return request(contract.providerConfiguration.origin+"/services/data/v61.0/sobjects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId),key);if(contract.providerPackId==="HUBSPOT_CRM_RECORD")return (await request("https://api.hubapi.com/crm/v3/objects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId)+"?properties="+encodeURIComponent(contract.criterion.field),key)).properties;if(contract.providerPackId==="POSTGRES_RECORD"){for(const name of [contract.providerConfiguration.viewName,contract.providerConfiguration.idColumn,contract.criterion.field])if(!/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(name))throw new Error("PostgreSQL identifier is invalid.");const {Client}=await import("pg");const client=new Client({connectionString:key,application_name:"witnora-read-only-probe"});await client.connect();try{await client.query("BEGIN READ ONLY");const result=await client.query({name:"witnora-provider-observe",text:'SELECT "'+contract.criterion.field+'" FROM "'+contract.providerConfiguration.viewName+'" WHERE "'+contract.providerConfiguration.idColumn+'" = $1 LIMIT 1',values:[resourceId]});await client.query("ROLLBACK");return result.rows[0]??{};}catch(error){await client.query("ROLLBACK").catch(()=>{});throw error;}finally{await client.end();}}throw new Error("Provider Harness contract is unsupported.");};
|
|
232
|
+
const observe=async(contract,resourceId,repository)=>{if(contract.sandboxObservation)return structuredClone(contract.sandboxObservation);const key=(await readFile(join(repository,contract.credentialHandle),"utf8")).trim();if(!key)throw new Error("The local read-only Provider credential is unavailable.");if(contract.providerPackId==="STRIPE_REFUND"){if(!key.startsWith("sk_test_"))throw new Error("Stripe test-mode credential is unavailable.");return request("https://api.stripe.com/v1/refunds/"+encodeURIComponent(resourceId),key);}if(contract.providerPackId==="SHOPIFY_DISPUTE"){const response=await fetch(contract.providerConfiguration.origin+"/admin/api/2025-07/shopify_payments/disputes/"+encodeURIComponent(resourceId)+".json",{method:"GET",headers:{accept:"application/json","x-shopify-access-token":key},redirect:"error",signal:AbortSignal.timeout(5000)});if(!response.ok)throw new Error("Provider read-only observation failed ("+response.status+").");return (await response.json()).dispute;}if(contract.providerPackId==="ZENDESK_TICKET")return (await request(contract.providerConfiguration.origin+"/api/v2/tickets/"+encodeURIComponent(resourceId)+".json",key)).ticket;if(contract.providerPackId==="SALESFORCE_RECORD")return request(contract.providerConfiguration.origin+"/services/data/v61.0/sobjects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId),key);if(contract.providerPackId==="HUBSPOT_CRM_RECORD")return (await request("https://api.hubapi.com/crm/v3/objects/"+encodeURIComponent(contract.providerConfiguration.resourceType)+"/"+encodeURIComponent(resourceId)+"?properties="+encodeURIComponent(contract.criterion.field),key)).properties;if(contract.providerPackId==="POSTGRES_RECORD"){for(const name of [contract.providerConfiguration.viewName,contract.providerConfiguration.idColumn,contract.criterion.field])if(!/^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(name))throw new Error("PostgreSQL identifier is invalid.");const {Client}=await import("pg");const client=new Client({connectionString:key,application_name:"witnora-read-only-probe"});await client.connect();try{await client.query("BEGIN READ ONLY");const result=await client.query({name:"witnora-provider-observe",text:'SELECT "'+contract.criterion.field+'" FROM "'+contract.providerConfiguration.viewName+'" WHERE "'+contract.providerConfiguration.idColumn+'" = $1 LIMIT 1',values:[resourceId]});await client.query("ROLLBACK");return result.rows[0]??{};}catch(error){await client.query("ROLLBACK").catch(()=>{});throw error;}finally{await client.end();}}throw new Error("Provider Harness contract is unsupported.");};
|
|
225
233
|
export function createWitnoraBusinessTaskEvaluatorOptions(context){return {
|
|
226
234
|
loadShadowObservations:async(task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const path=join(context.repository,".witnora","scenarios",task.id+".json");const value=JSON.parse(await readFile(path,"utf8"));if(!Array.isArray(value)||value.length>100)throw new Error("Local scenario file is invalid.");return value;},
|
|
227
235
|
evaluateShadowCandidate:async(candidate,task)=>{const contract=contracts.find((item)=>item.taskContractId===task.id);if(!contract)throw new Error("No exact real-path contract.");const resourceId=String(candidate.input?.resourceId??"");if(!/^[A-Za-z0-9._:-]{1,200}$/.test(resourceId))throw new Error("Provider resourceId is invalid.");for(const pathId of contract.actionPathIds)candidate.propose({pathId,parametersDigestSha256:sha(resourceId)});const raw=await observe(contract,resourceId,context.repository);const actual=raw?.[contract.criterion.field];const observed=actual===undefined?{}:{[contract.criterion.field]:actual};return {resultDigestSha256:sha(observed),criteria:[{id:contract.criterion.id,passed:actual===contract.criterion.expected}]};}
|
|
@@ -230,6 +238,8 @@ export async function checkWitnoraProviderHealth(context){for(const contract of
|
|
|
230
238
|
`;
|
|
231
239
|
}
|
|
232
240
|
async function persistProviderCredential(repository, plan, environment) {
|
|
241
|
+
if (plan.generated.providerPackId === "SHOPIFY_DISPUTE" && plan.environment === "sandbox" && plan.generated.providerConfiguration === undefined)
|
|
242
|
+
return;
|
|
233
243
|
const name = providerCredentialEnvironmentName(plan.generated.providerPackId);
|
|
234
244
|
const value = environment[name];
|
|
235
245
|
if (!value)
|