@apilift/cli 1.4.39 → 1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc
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/apilift-collector.js +10 -3
- package/dist/apilift-native-host.js +10 -3
- package/dist/apilift.js +256 -101
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/skills/discover/SKILL.md +2 -2
- package/dist/plugin/skills/discover/references/operation-contract.md +6 -2
- package/dist/plugin/skills/use/SKILL.md +1 -1
- package/package.json +2 -2
|
@@ -27953,7 +27953,7 @@ function parseUrl(value) {
|
|
|
27953
27953
|
function loopback(host) {
|
|
27954
27954
|
return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
|
|
27955
27955
|
}
|
|
27956
|
-
var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
|
|
27956
|
+
var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofPointer, proofComparison, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
|
|
27957
27957
|
var init_schema = __esm({
|
|
27958
27958
|
"../../packages/core/dist/definitions/schema.js"() {
|
|
27959
27959
|
"use strict";
|
|
@@ -28011,9 +28011,13 @@ var init_schema = __esm({
|
|
|
28011
28011
|
if (v2.generated && (v2.path !== void 0 || v2.fields !== void 0))
|
|
28012
28012
|
ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
|
|
28013
28013
|
});
|
|
28014
|
-
|
|
28014
|
+
proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
|
|
28015
|
+
proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
|
|
28016
|
+
proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v2, ctx) => {
|
|
28015
28017
|
if (!!v2.equals === !!v2.absent)
|
|
28016
28018
|
ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
|
|
28019
|
+
if (v2.select && v2.absent)
|
|
28020
|
+
ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
|
|
28017
28021
|
if (!v2.effect && !["GET", "HEAD"].includes(v2.request.method))
|
|
28018
28022
|
ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
|
|
28019
28023
|
});
|
|
@@ -28350,7 +28354,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
|
|
|
28350
28354
|
const transport = async (input2, init) => {
|
|
28351
28355
|
const method = init?.method ?? "GET";
|
|
28352
28356
|
const headers = new Headers(init?.headers);
|
|
28353
|
-
headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
|
|
28357
|
+
headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
|
|
28354
28358
|
headers.set("Apilift-Client", `cli/${cliVersion}`);
|
|
28355
28359
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
28356
28360
|
if (sessionId) headers.set("Apilift-Session", sessionId);
|
|
@@ -28454,6 +28458,9 @@ var init_baselines = __esm({
|
|
|
28454
28458
|
startPreparation(sessionId, appSlug, at = /* @__PURE__ */ new Date()) {
|
|
28455
28459
|
this.ledger.db.prepare("INSERT OR IGNORE INTO baseline_preparation_start (session_id,app_slug,started_at) VALUES (?,?,?)").run(sessionId, appSlug, at.toISOString());
|
|
28456
28460
|
}
|
|
28461
|
+
clearPreparation(sessionId, appSlug) {
|
|
28462
|
+
this.ledger.db.prepare("DELETE FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").run(sessionId, appSlug);
|
|
28463
|
+
}
|
|
28457
28464
|
finishPreparation(sessionId, appSlug, appId, method, at = /* @__PURE__ */ new Date()) {
|
|
28458
28465
|
const row = this.ledger.db.prepare("SELECT started_at FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").get(sessionId, appSlug);
|
|
28459
28466
|
if (!row || this.ledger.db.prepare("SELECT 1 FROM baseline_candidate WHERE draft_id=?").get(`preparation:${sessionId}:${appId}:${method}`)) return;
|
|
@@ -29457,9 +29457,13 @@ var responseSchema = external_exports.strictObject({ schema: jsonSchema.optional
|
|
|
29457
29457
|
if (v.generated && (v.path !== void 0 || v.fields !== void 0))
|
|
29458
29458
|
ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
|
|
29459
29459
|
});
|
|
29460
|
-
var
|
|
29460
|
+
var proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
|
|
29461
|
+
var proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
|
|
29462
|
+
var proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
|
|
29461
29463
|
if (!!v.equals === !!v.absent)
|
|
29462
29464
|
ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
|
|
29465
|
+
if (v.select && v.absent)
|
|
29466
|
+
ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
|
|
29463
29467
|
if (!v.effect && !["GET", "HEAD"].includes(v.request.method))
|
|
29464
29468
|
ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
|
|
29465
29469
|
});
|
|
@@ -29559,7 +29563,7 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
|
|
|
29559
29563
|
continue;
|
|
29560
29564
|
if (proof && token.startsWith("result.") && pointerSchema.safeParse(token.slice(7)).success)
|
|
29561
29565
|
continue;
|
|
29562
|
-
issues.push({ path: path2, message: `Unknown template ${match[0]}` });
|
|
29566
|
+
issues.push({ path: path2, message: `Unknown template ${match[0]}${proof ? '; result URLs use {result./id}, and JSON values use {"$result":"/id"}' : ""}` });
|
|
29563
29567
|
}
|
|
29564
29568
|
};
|
|
29565
29569
|
const walk = (value, path2) => {
|
|
@@ -29630,6 +29634,9 @@ function validateDefinition(kind, name, value) {
|
|
|
29630
29634
|
for (const [i, comparison] of (definition2.proof?.equals ?? []).entries())
|
|
29631
29635
|
if ("input" in comparison && !names2.includes(comparison.input))
|
|
29632
29636
|
issues.push({ path: `/proof/equals/${i}/input`, message: "Unknown input" });
|
|
29637
|
+
for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
|
|
29638
|
+
if ("input" in comparison && !names2.includes(comparison.input))
|
|
29639
|
+
issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
|
|
29633
29640
|
if (definition2.response?.generated && !generated.has(definition2.response.generated))
|
|
29634
29641
|
issues.push({ path: "/response/generated", message: "Unknown generated value" });
|
|
29635
29642
|
}
|
|
@@ -29814,7 +29821,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
|
|
|
29814
29821
|
const transport = async (input2, init) => {
|
|
29815
29822
|
const method = init?.method ?? "GET";
|
|
29816
29823
|
const headers = new Headers(init?.headers);
|
|
29817
|
-
headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
|
|
29824
|
+
headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
|
|
29818
29825
|
headers.set("Apilift-Client", `cli/${cliVersion}`);
|
|
29819
29826
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
29820
29827
|
if (sessionId) headers.set("Apilift-Session", sessionId);
|
package/dist/apilift.js
CHANGED
|
@@ -28017,7 +28017,7 @@ function parseUrl(value) {
|
|
|
28017
28017
|
function loopback(host) {
|
|
28018
28018
|
return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
|
|
28019
28019
|
}
|
|
28020
|
-
var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
|
|
28020
|
+
var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofPointer, proofComparison, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
|
|
28021
28021
|
var init_schema = __esm({
|
|
28022
28022
|
"../../packages/core/dist/definitions/schema.js"() {
|
|
28023
28023
|
"use strict";
|
|
@@ -28075,9 +28075,13 @@ var init_schema = __esm({
|
|
|
28075
28075
|
if (v.generated && (v.path !== void 0 || v.fields !== void 0))
|
|
28076
28076
|
ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
|
|
28077
28077
|
});
|
|
28078
|
-
|
|
28078
|
+
proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
|
|
28079
|
+
proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
|
|
28080
|
+
proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
|
|
28079
28081
|
if (!!v.equals === !!v.absent)
|
|
28080
28082
|
ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
|
|
28083
|
+
if (v.select && v.absent)
|
|
28084
|
+
ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
|
|
28081
28085
|
if (!v.effect && !["GET", "HEAD"].includes(v.request.method))
|
|
28082
28086
|
ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
|
|
28083
28087
|
});
|
|
@@ -28411,7 +28415,7 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
|
|
|
28411
28415
|
continue;
|
|
28412
28416
|
if (proof && token.startsWith("result.") && pointerSchema.safeParse(token.slice(7)).success)
|
|
28413
28417
|
continue;
|
|
28414
|
-
issues.push({ path: path2, message: `Unknown template ${match[0]}` });
|
|
28418
|
+
issues.push({ path: path2, message: `Unknown template ${match[0]}${proof ? '; result URLs use {result./id}, and JSON values use {"$result":"/id"}' : ""}` });
|
|
28415
28419
|
}
|
|
28416
28420
|
};
|
|
28417
28421
|
const walk = (value, path2) => {
|
|
@@ -28482,6 +28486,9 @@ function validateDefinition(kind, name, value) {
|
|
|
28482
28486
|
for (const [i, comparison] of (definition2.proof?.equals ?? []).entries())
|
|
28483
28487
|
if ("input" in comparison && !names2.includes(comparison.input))
|
|
28484
28488
|
issues.push({ path: `/proof/equals/${i}/input`, message: "Unknown input" });
|
|
28489
|
+
for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
|
|
28490
|
+
if ("input" in comparison && !names2.includes(comparison.input))
|
|
28491
|
+
issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
|
|
28485
28492
|
if (definition2.response?.generated && !generated.has(definition2.response.generated))
|
|
28486
28493
|
issues.push({ path: "/response/generated", message: "Unknown generated value" });
|
|
28487
28494
|
}
|
|
@@ -28806,7 +28813,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
|
|
|
28806
28813
|
const transport = async (input2, init) => {
|
|
28807
28814
|
const method = init?.method ?? "GET";
|
|
28808
28815
|
const headers = new Headers(init?.headers);
|
|
28809
|
-
headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
|
|
28816
|
+
headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
|
|
28810
28817
|
headers.set("Apilift-Client", `cli/${cliVersion}`);
|
|
28811
28818
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
28812
28819
|
if (sessionId) headers.set("Apilift-Session", sessionId);
|
|
@@ -28961,7 +28968,7 @@ var init_registry = __esm({
|
|
|
28961
28968
|
contextClear: { path: ["context", "clear"], min: 0, max: 0, values: ["app"] },
|
|
28962
28969
|
discoverDraft: { path: ["discover", "draft"], min: 1, max: 2 },
|
|
28963
28970
|
discoverCheck: { path: ["discover", "check"], min: 0, max: 0 },
|
|
28964
|
-
discoverRun: { path: ["discover", "run"], min: 0, max: 0 },
|
|
28971
|
+
discoverRun: { path: ["discover", "run"], min: 0, max: 0, flags: ["proof-only"] },
|
|
28965
28972
|
discoverPublish: { path: ["discover", "publish"], min: 0, max: 0 },
|
|
28966
28973
|
discoverStatus: { path: ["discover", "status"], min: 0, max: 0 },
|
|
28967
28974
|
discoverDefer: { path: ["discover", "defer"], min: 0, max: 0 },
|
|
@@ -29230,6 +29237,9 @@ var init_baselines = __esm({
|
|
|
29230
29237
|
startPreparation(sessionId, appSlug, at = /* @__PURE__ */ new Date()) {
|
|
29231
29238
|
this.ledger.db.prepare("INSERT OR IGNORE INTO baseline_preparation_start (session_id,app_slug,started_at) VALUES (?,?,?)").run(sessionId, appSlug, at.toISOString());
|
|
29232
29239
|
}
|
|
29240
|
+
clearPreparation(sessionId, appSlug) {
|
|
29241
|
+
this.ledger.db.prepare("DELETE FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").run(sessionId, appSlug);
|
|
29242
|
+
}
|
|
29233
29243
|
finishPreparation(sessionId, appSlug, appId, method, at = /* @__PURE__ */ new Date()) {
|
|
29234
29244
|
const row = this.ledger.db.prepare("SELECT started_at FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").get(sessionId, appSlug);
|
|
29235
29245
|
if (!row || this.ledger.db.prepare("SELECT 1 FROM baseline_candidate WHERE draft_id=?").get(`preparation:${sessionId}:${appId}:${method}`)) return;
|
|
@@ -30784,7 +30794,7 @@ function draftTemplate(kind) {
|
|
|
30784
30794
|
""
|
|
30785
30795
|
].join("\n") : "";
|
|
30786
30796
|
const tabGuidance = ["action", "transport"].includes(kind) ? "// capture.tab is a selector returned by browser tabs; it selects the same signed-in Chrome tab for capture and discover run, including connection probes. Leave other sessions' tabs untouched.\n" : "";
|
|
30787
|
-
const captureGuidance = kind === "action" ? '// Public descriptions explain behavior, consequences and limits; do not instruct consumers to repeat runtime proof or browser checks. Author verification is separate from consumer judgment.\n// Request JSON/query uses typed {"$input":"name"} bindings; URL segments use {input.name}. Resolve every {unresolved.N} marker from retained evidence before running.\n//
|
|
30797
|
+
const captureGuidance = kind === "action" ? '// Public descriptions explain behavior, consequences and limits; do not instruct consumers to repeat runtime proof or browser checks. Author verification is separate from consumer judgment.\n// Request JSON/query uses typed {"$input":"name"} bindings; URL segments use {input.name}. Resolve every {unresolved.N} marker from retained evidence before running.\n// Discovery-only mutation proof compares a readback path with exactly one input, primary result path or internal JSON value: equals: [{"path":"/ready","value":true}]. Public literals require semantic review; fixed proof expectations are not caller flags.\n// Proof URLs bind returned identifiers with {result./id}; JSON bodies/query use {"$result":"/id"}. Pointers are literal, never templates.\n// For one member of a nested list: select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}; equals paths then refer to the single matching member. Compare identity and meaningful changed fields, not entire objects.\n// Repair a failed response/proof with discover run --proof-only; it reuses the saved primary response and sends only the readback. Publication still needs a successful test and accepted semantic review.\n// For large collections, investigate native search/filter/detail routes before enumeration. Search saved results locally and inspect only matching records; preserve structured dynamic fields.\n// After selecting a captured request, discover capture auth runs a bounded default search without authField or authScope. Set either only to narrow an observed field or location; browser-managed cookies need no locator.\n' : "";
|
|
30788
30798
|
return `${head}${probeGuidance}${tabGuidance}${captureGuidance}${JSON.stringify(templates[kind], null, 2)}
|
|
30789
30799
|
`;
|
|
30790
30800
|
}
|
|
@@ -30803,8 +30813,8 @@ var init_envelope = __esm({
|
|
|
30803
30813
|
|
|
30804
30814
|
// src/discovery/drafts.ts
|
|
30805
30815
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
30806
|
-
import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
30807
|
-
import { dirname as dirname2, join as join5 } from "node:path";
|
|
30816
|
+
import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
30817
|
+
import { basename, dirname as dirname2, join as join5 } from "node:path";
|
|
30808
30818
|
function pathFor(root, appSlug, kind, name) {
|
|
30809
30819
|
if (!slugPattern.test(appSlug)) throw new Error("App slug is invalid");
|
|
30810
30820
|
if (kind === "action" ? !actionPattern.test(name) : kind !== "app" && !resourcePattern.test(name)) throw new Error("Resource name is invalid");
|
|
@@ -30860,12 +30870,65 @@ var init_drafts = __esm({
|
|
|
30860
30870
|
if (!slugPattern.test(appSlug)) throw new Error("App slug is invalid");
|
|
30861
30871
|
const current = this.session(sessionId);
|
|
30862
30872
|
if (current && current.appSlug !== appSlug) throw new DraftConflict("selected-draft", `Discovery is open for ${current.appSlug}; close it before opening another app`);
|
|
30863
|
-
|
|
30864
|
-
|
|
30873
|
+
const moved = [];
|
|
30874
|
+
this.state.db.exec("BEGIN IMMEDIATE");
|
|
30875
|
+
try {
|
|
30876
|
+
const stale = this.retained(appSlug).filter((draft) => draft.appId !== appId);
|
|
30877
|
+
for (const draft of stale) {
|
|
30878
|
+
if (draft.ownerSessionId && draft.ownerSessionId !== sessionId) throw new DraftConflict("owned-draft", `The previous app's ${draft.kind} ${draft.name} is owned by another agent; that agent must finish or defer it before reopening this slug`);
|
|
30879
|
+
if (this.publication(draft.id)) throw new DraftConflict("selected-draft", "A previous app publication may have been sent; its owner must reconcile discover publish before reopening this slug");
|
|
30880
|
+
if (this.hasTable("discovery_capture")) {
|
|
30881
|
+
const capture = this.state.db.prepare("SELECT scope_id FROM discovery_capture WHERE draft_id=?").get(draft.id);
|
|
30882
|
+
if (capture?.scope_id) throw new DraftConflict("selected-draft", "A previous app recording is still active or unconfirmed; its owner must settle discover capture cancel before reopening this slug");
|
|
30883
|
+
}
|
|
30884
|
+
}
|
|
30885
|
+
const previousSessions = this.state.db.prepare("SELECT session_id FROM discovery_session WHERE app_slug=? AND app_id IS NOT ?").all(appSlug, appId);
|
|
30886
|
+
const changed = stale.length > 0 || previousSessions.length > 0;
|
|
30887
|
+
const archivedDrafts = changed ? join5(this.root, "retired-drafts", randomUUID4()) : void 0;
|
|
30888
|
+
if (archivedDrafts) mkdirSync5(archivedDrafts, { recursive: true, mode: 448 });
|
|
30889
|
+
for (const draft of stale) {
|
|
30890
|
+
if (existsSync2(draft.filePath)) {
|
|
30891
|
+
const archived = join5(archivedDrafts, paths[draft.kind], basename(draft.filePath));
|
|
30892
|
+
mkdirSync5(dirname2(archived), { recursive: true, mode: 448 });
|
|
30893
|
+
renameSync(draft.filePath, archived);
|
|
30894
|
+
moved.push({ from: draft.filePath, to: archived });
|
|
30895
|
+
}
|
|
30896
|
+
for (const table of ["selected_discovery_draft", "discovery_publication", "discovery_evidence", "discovery_attempt", "discovery_capture"]) {
|
|
30897
|
+
if (this.hasTable(table)) this.state.db.prepare(`DELETE FROM ${table} WHERE draft_id=?`).run(draft.id);
|
|
30898
|
+
}
|
|
30899
|
+
this.state.db.prepare("DELETE FROM discovery_draft WHERE id=?").run(draft.id);
|
|
30900
|
+
}
|
|
30901
|
+
if (changed && this.hasTable("discovery_research")) {
|
|
30902
|
+
const research = this.state.db.prepare("SELECT file_path FROM discovery_research WHERE app_slug=?").all(appSlug);
|
|
30903
|
+
for (const row of research) if (existsSync2(row.file_path)) {
|
|
30904
|
+
const archived = join5(archivedDrafts, "api", basename(row.file_path));
|
|
30905
|
+
mkdirSync5(dirname2(archived), { recursive: true, mode: 448 });
|
|
30906
|
+
renameSync(row.file_path, archived);
|
|
30907
|
+
moved.push({ from: row.file_path, to: archived });
|
|
30908
|
+
}
|
|
30909
|
+
if (this.hasTable("selected_discovery_research")) this.state.db.prepare("DELETE FROM selected_discovery_research WHERE app_slug=?").run(appSlug);
|
|
30910
|
+
this.state.db.prepare("DELETE FROM discovery_research WHERE app_slug=?").run(appSlug);
|
|
30911
|
+
}
|
|
30912
|
+
this.state.db.prepare("DELETE FROM discovery_session WHERE app_slug=? AND app_id IS NOT ?").run(appSlug, appId);
|
|
30913
|
+
this.state.db.prepare("INSERT INTO discovery_session (session_id,app_slug,app_id,title) VALUES (?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET app_id=excluded.app_id,title=excluded.title").run(sessionId, appSlug, appId, title2);
|
|
30914
|
+
this.state.db.exec("COMMIT");
|
|
30915
|
+
if (changed) withBaselines(this.root, (baselines) => {
|
|
30916
|
+
for (const draft of stale) baselines.closeObservation(draft.id, "app-identity-changed");
|
|
30917
|
+
for (const id2 of /* @__PURE__ */ new Set([sessionId, ...previousSessions.map((row) => row.session_id)])) baselines.clearPreparation(id2, appSlug);
|
|
30918
|
+
});
|
|
30919
|
+
return { ...this.session(sessionId), ...archivedDrafts ? { archivedDrafts } : {} };
|
|
30920
|
+
} catch (error62) {
|
|
30921
|
+
this.state.db.exec("ROLLBACK");
|
|
30922
|
+
for (const { from, to } of moved.reverse()) renameSync(to, from);
|
|
30923
|
+
throw error62;
|
|
30924
|
+
}
|
|
30925
|
+
}
|
|
30926
|
+
hasTable(name) {
|
|
30927
|
+
return Boolean(this.state.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
|
|
30865
30928
|
}
|
|
30866
30929
|
setAppId(appSlug, appId) {
|
|
30867
|
-
this.state.db.prepare("UPDATE discovery_session SET app_id=? WHERE app_slug=?").run(appId, appSlug);
|
|
30868
|
-
this.state.db.prepare("UPDATE discovery_draft SET app_id=? WHERE app_slug=?").run(appId, appSlug);
|
|
30930
|
+
this.state.db.prepare("UPDATE discovery_session SET app_id=? WHERE app_slug=? AND app_id IS NULL").run(appId, appSlug);
|
|
30931
|
+
this.state.db.prepare("UPDATE discovery_draft SET app_id=? WHERE app_slug=? AND app_id IS NULL").run(appId, appSlug);
|
|
30869
30932
|
}
|
|
30870
30933
|
close(sessionId) {
|
|
30871
30934
|
const current = this.session(sessionId);
|
|
@@ -30909,6 +30972,18 @@ var init_drafts = __esm({
|
|
|
30909
30972
|
saveAttempt(draftId, fingerprint2, state) {
|
|
30910
30973
|
this.state.db.prepare("INSERT INTO discovery_attempt (draft_id,fingerprint,state,recorded_at) VALUES (?,?,?,?) ON CONFLICT(draft_id) DO UPDATE SET fingerprint=excluded.fingerprint,state=excluded.state,recorded_at=excluded.recorded_at").run(draftId, fingerprint2, state, (/* @__PURE__ */ new Date()).toISOString());
|
|
30911
30974
|
}
|
|
30975
|
+
requestAttempts() {
|
|
30976
|
+
this.state.db.exec("CREATE TABLE IF NOT EXISTS discovery_request_attempt (draft_id TEXT NOT NULL REFERENCES discovery_draft(id) ON DELETE CASCADE, fingerprint TEXT NOT NULL, state TEXT NOT NULL, response_path TEXT, response_digest TEXT, PRIMARY KEY(draft_id,fingerprint))");
|
|
30977
|
+
}
|
|
30978
|
+
requestAttempt(draftId, fingerprint2) {
|
|
30979
|
+
this.requestAttempts();
|
|
30980
|
+
const row = this.state.db.prepare("SELECT state,response_path,response_digest FROM discovery_request_attempt WHERE draft_id=? AND fingerprint=?").get(draftId, fingerprint2);
|
|
30981
|
+
return row ? { state: row.state, responsePath: row.response_path, responseDigest: row.response_digest } : null;
|
|
30982
|
+
}
|
|
30983
|
+
saveRequestAttempt(draftId, fingerprint2, state, responsePath, responseDigest) {
|
|
30984
|
+
this.requestAttempts();
|
|
30985
|
+
this.state.db.prepare("INSERT INTO discovery_request_attempt (draft_id,fingerprint,state,response_path,response_digest) VALUES (?,?,?,?,?) ON CONFLICT(draft_id,fingerprint) DO UPDATE SET state=excluded.state,response_path=COALESCE(excluded.response_path,discovery_request_attempt.response_path),response_digest=COALESCE(excluded.response_digest,discovery_request_attempt.response_digest)").run(draftId, fingerprint2, state, responsePath ?? null, responseDigest ?? null);
|
|
30986
|
+
}
|
|
30912
30987
|
discard(sessionId) {
|
|
30913
30988
|
const draft = this.selected(sessionId);
|
|
30914
30989
|
if (!draft) return null;
|
|
@@ -30958,6 +31033,7 @@ var init_drafts = __esm({
|
|
|
30958
31033
|
if (selected && (selected.appSlug !== input2.appSlug || selected.kind !== input2.kind || selected.name !== name)) throw new DraftConflict("selected-draft", `This agent already selected ${selected.kind} ${selected.name}; defer or finish it before opening another resource`);
|
|
30959
31034
|
const existing = this.state.db.prepare("SELECT * FROM discovery_draft WHERE app_slug=? AND kind=? AND name=?").get(input2.appSlug, input2.kind, name);
|
|
30960
31035
|
if (existing?.owner_session_id && existing.owner_session_id !== input2.sessionId) throw new DraftConflict("owned-draft", `${input2.kind} ${name} is owned by another agent session; its file was preserved`);
|
|
31036
|
+
if (existing && existing.app_id !== (input2.appId ?? null)) throw new DraftConflict("selected-draft", "The retained draft belongs to a different app; reopen the app from its current registry identity before editing");
|
|
30961
31037
|
if (existing) filePath = String(existing.file_path);
|
|
30962
31038
|
const id2 = existing ? String(existing.id) : randomUUID4();
|
|
30963
31039
|
if (!existing) this.state.db.prepare("INSERT INTO discovery_draft (id,app_slug,app_id,kind,name,owner_session_id,file_path,base_version) VALUES (?,?,?,?,?,?,?,?)").run(id2, input2.appSlug, input2.appId ?? null, input2.kind, name, input2.sessionId, filePath, input2.baseVersion ?? null);
|
|
@@ -32367,7 +32443,7 @@ var init_vault = __esm({
|
|
|
32367
32443
|
|
|
32368
32444
|
// src/commands/run.ts
|
|
32369
32445
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
32370
|
-
import { readFileSync as readFileSync7, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
|
|
32446
|
+
import { readFileSync as readFileSync7, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
|
|
32371
32447
|
import { dirname as dirname5, resolve as resolve4, join as join10 } from "node:path";
|
|
32372
32448
|
function pointer2(value, path2) {
|
|
32373
32449
|
if (path2 === "") return value;
|
|
@@ -32375,7 +32451,7 @@ function pointer2(value, path2) {
|
|
|
32375
32451
|
let current = value;
|
|
32376
32452
|
for (const raw of path2.slice(1).split("/")) {
|
|
32377
32453
|
const name = raw.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
32378
|
-
if (typeof current !== "object" || current === null || !(name
|
|
32454
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, name)) throw new Error(`Result path ${path2} is missing`);
|
|
32379
32455
|
current = current[name];
|
|
32380
32456
|
}
|
|
32381
32457
|
return current;
|
|
@@ -32584,7 +32660,7 @@ function writeOutput(path2, content) {
|
|
|
32584
32660
|
const pending = join10(dirname5(path2), `.apilift-output-${randomUUID7()}`);
|
|
32585
32661
|
try {
|
|
32586
32662
|
writeFileSync6(pending, content, { flag: "wx", mode: 384 });
|
|
32587
|
-
|
|
32663
|
+
renameSync2(pending, path2);
|
|
32588
32664
|
} catch (error62) {
|
|
32589
32665
|
try {
|
|
32590
32666
|
unlinkSync3(pending);
|
|
@@ -32611,7 +32687,19 @@ function projectResponse(body, action, generated) {
|
|
|
32611
32687
|
}
|
|
32612
32688
|
if (response?.schema) {
|
|
32613
32689
|
const validate2 = ajv3.compile(response.schema);
|
|
32614
|
-
if (!validate2(value))
|
|
32690
|
+
if (!validate2(value)) {
|
|
32691
|
+
const issues = (validate2.errors ?? []).slice(0, 5).map((error62) => {
|
|
32692
|
+
let actual;
|
|
32693
|
+
try {
|
|
32694
|
+
actual = pointer2(value, error62.instancePath);
|
|
32695
|
+
} catch {
|
|
32696
|
+
actual = void 0;
|
|
32697
|
+
}
|
|
32698
|
+
const type = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual;
|
|
32699
|
+
return `${error62.instancePath || "/"}: ${error62.message ?? "schema mismatch"}; actual type ${type}`;
|
|
32700
|
+
});
|
|
32701
|
+
throw new Error(`Response contract mismatch: ${issues.join("; ")}`);
|
|
32702
|
+
}
|
|
32615
32703
|
}
|
|
32616
32704
|
return value;
|
|
32617
32705
|
}
|
|
@@ -32688,14 +32776,34 @@ async function sendBrowser(root, appId, contextName, pageOrigin, effect2, transp
|
|
|
32688
32776
|
if (!selection.execution) await client.closeScope(scope2.id);
|
|
32689
32777
|
}
|
|
32690
32778
|
}
|
|
32691
|
-
function
|
|
32692
|
-
if (proof.absent)
|
|
32693
|
-
|
|
32694
|
-
|
|
32695
|
-
|
|
32779
|
+
function confirmProof(proof, body, primary, inputs, status) {
|
|
32780
|
+
if (proof.absent) {
|
|
32781
|
+
if (status !== 404) throw new ResponseContractFailure(`Absence proof expected HTTP 404; received HTTP ${status}`);
|
|
32782
|
+
return;
|
|
32783
|
+
}
|
|
32784
|
+
if (status < 200 || status >= 300) throw new ResponseContractFailure(`Readback returned HTTP ${status}`);
|
|
32785
|
+
const matches = (comparison, selected2, missingIsMismatch = false) => {
|
|
32696
32786
|
const expected = "input" in comparison ? inputs[comparison.input] : "result" in comparison ? pointer2(primary, comparison.result) : comparison.value;
|
|
32787
|
+
let actual;
|
|
32788
|
+
try {
|
|
32789
|
+
actual = pointer2(selected2, comparison.path);
|
|
32790
|
+
} catch (error62) {
|
|
32791
|
+
if (missingIsMismatch) return false;
|
|
32792
|
+
throw error62;
|
|
32793
|
+
}
|
|
32697
32794
|
return actual !== void 0 && expected !== void 0 && canonicalJson(actual) === canonicalJson(expected);
|
|
32698
|
-
}
|
|
32795
|
+
};
|
|
32796
|
+
let selected = body;
|
|
32797
|
+
if (proof.select) {
|
|
32798
|
+
const collection = pointer2(body, proof.select.path);
|
|
32799
|
+
if (!Array.isArray(collection)) throw new ResponseContractFailure(`Readback selection ${proof.select.path || "/"} must be an array`);
|
|
32800
|
+
const found = collection.filter((item) => proof.select.where.every((comparison) => matches(comparison, item, true)));
|
|
32801
|
+
if (found.length !== 1) throw new ResponseContractFailure(`Readback selection matched ${found.length} items; expected exactly one`);
|
|
32802
|
+
selected = found[0];
|
|
32803
|
+
}
|
|
32804
|
+
for (const comparison of proof.equals ?? []) {
|
|
32805
|
+
if (!matches(comparison, selected)) throw new ResponseContractFailure(`Readback comparison failed at ${comparison.path || "/"}`);
|
|
32806
|
+
}
|
|
32699
32807
|
}
|
|
32700
32808
|
async function handleRun(invocation, api, state, session, fetcher, root, stdin = () => readFileSync7(0, "utf8"), onReport) {
|
|
32701
32809
|
const [appName, actionName] = invocation.positionals;
|
|
@@ -32768,7 +32876,7 @@ No app request was sent.
|
|
|
32768
32876
|
const transportVersion = bundle2.closure.versions.find((item) => item.kind === "transport" && item.name === (action && "transport" in action ? action.transport : ""));
|
|
32769
32877
|
const transport = transportVersion?.definition;
|
|
32770
32878
|
if (!action || !("request" in action) || !transport) return fail2("execution-bundle-invalid \u2014 The selected action or transport is missing. No app request was sent.\n", 5);
|
|
32771
|
-
if (action.effect !== "read" && !action.proof) return fail2("execution-bundle-invalid \u2014 A mutating action has no
|
|
32879
|
+
if (action.effect !== "read" && !action.proof) return fail2("execution-bundle-invalid \u2014 A mutating action has no discovery proof specification. No app request was sent.\n", 5);
|
|
32772
32880
|
const selectedCredential = state.useSession(session.id, bundle2.app.id)?.credentials[action.transport];
|
|
32773
32881
|
if (transport.credentials?.length && !selectedCredential) return fail2(`credential-required \u2014 ${actionName} needs a selected credential for ${action.transport}. Run ${prefix(`credentials list ${appName} ${action.transport}`)}, then credentials use or add. No app request was sent.
|
|
32774
32882
|
`, 5);
|
|
@@ -32840,7 +32948,6 @@ Inspect current action documentation: ${prefix(`actions show ${appName} ${action
|
|
|
32840
32948
|
let outcome = "network-error";
|
|
32841
32949
|
let diagnostic;
|
|
32842
32950
|
let output2;
|
|
32843
|
-
let verified;
|
|
32844
32951
|
try {
|
|
32845
32952
|
const primary = await sendRequest(prepared, action.effect);
|
|
32846
32953
|
if (!primary.response.ok) {
|
|
@@ -32849,29 +32956,9 @@ Inspect current action documentation: ${prefix(`actions show ${appName} ${action
|
|
|
32849
32956
|
${action.effect !== "read" && primary.response.status >= 500 ? readRecovery : ""}`, 4, true);
|
|
32850
32957
|
} else {
|
|
32851
32958
|
const value = projected(primary.body, action, generated);
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
const proof = await timed(timings, "proofMs", () => sendRequest(preparedProof, "read"));
|
|
32855
|
-
let passed = false;
|
|
32856
|
-
try {
|
|
32857
|
-
passed = proofPassed(action.proof, proof.body, value, inputs.values, proof.response.status);
|
|
32858
|
-
} catch {
|
|
32859
|
-
}
|
|
32860
|
-
if (!passed) {
|
|
32861
|
-
outcome = "interface-error";
|
|
32862
|
-
diagnostic = "proof-failed";
|
|
32863
|
-
output2 = fail2(`proof-failed \u2014 ${actionName} was sent, but its readback did not confirm success. ${readRecovery}`, 12, true);
|
|
32864
|
-
} else {
|
|
32865
|
-
outcome = "success";
|
|
32866
|
-
verified = action.proof.absent ? "absence" : "readback";
|
|
32867
|
-
output2 = result(`${JSON.stringify(value, null, 2)}
|
|
32868
|
-
`, true);
|
|
32869
|
-
}
|
|
32870
|
-
} else {
|
|
32871
|
-
outcome = "success";
|
|
32872
|
-
output2 = result(`${JSON.stringify(value, null, 2)}
|
|
32959
|
+
outcome = "success";
|
|
32960
|
+
output2 = result(`${JSON.stringify(value, null, 2)}
|
|
32873
32961
|
`, true);
|
|
32874
|
-
}
|
|
32875
32962
|
}
|
|
32876
32963
|
} catch (error62) {
|
|
32877
32964
|
if (error62 instanceof ResponseContractFailure && dispatchedAt) {
|
|
@@ -32890,7 +32977,7 @@ Inspect saved entries: ${prefix(`credentials list ${appName} ${action.transport}
|
|
|
32890
32977
|
}
|
|
32891
32978
|
if (browserExecution.scope) await liveBrowser?.closeScope(browserExecution.scope.id).catch(() => {
|
|
32892
32979
|
});
|
|
32893
|
-
const report = { id: reportId, connectionFingerprint: await hashJson({ transport: transportVersion.executableHash, credential: selectedCredential ?? null, browser: browserPage?.pageOrigin ?? null }), ...bundle2.receipt ? { receiptId: bundle2.receipt.id } : {},
|
|
32980
|
+
const report = { id: reportId, connectionFingerprint: await hashJson({ transport: transportVersion.executableHash, credential: selectedCredential ?? null, browser: browserPage?.pageOrigin ?? null }), ...bundle2.receipt ? { receiptId: bundle2.receipt.id } : {}, appId: bundle2.app.id, actionName, actionVersion: actionVersion.version, closureHash: bundle2.closure.hash, purpose: "run", clientFamily: "cli", agent: session.agent, outcome, ...diagnostic ? { diagnostic } : {}, startedAt, ...dispatchedAt ? { dispatchedAt } : {}, finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
32894
32981
|
try {
|
|
32895
32982
|
state.queueExecutionReport(reportId, report);
|
|
32896
32983
|
onReport?.(reportId);
|
|
@@ -32921,21 +33008,33 @@ Inspect saved entries: ${prefix(`credentials list ${appName} ${action.transport}
|
|
|
32921
33008
|
saved = readFileSync7(recoveryPath, "utf8") === completeResult;
|
|
32922
33009
|
} catch {
|
|
32923
33010
|
}
|
|
32924
|
-
output2 = { ...fail2(`output-failed \u2014 The app request completed, but the result could not be saved or selected: ${error62 instanceof Error ? error62.message : "unknown error"}. ${saved ? `Complete result: ${recoveryPath}. Read that local JSON file to recover the returned identifiers` : "The complete result is on stdout"}
|
|
33011
|
+
output2 = { ...fail2(`output-failed \u2014 The app request completed, but the result could not be saved or selected: ${error62 instanceof Error ? error62.message : "unknown error"}. ${saved ? `Complete result: ${recoveryPath}. Read that local JSON file to recover the returned identifiers` : "The complete result is on stdout"}. Do not repeat the app mutation.
|
|
32925
33012
|
`, 12, true), ...saved ? {} : { stdout: completeResult, stdoutJson: true } };
|
|
32926
33013
|
}
|
|
32927
33014
|
}
|
|
32928
|
-
if (verified && output2.exitCode === 0) {
|
|
32929
|
-
output2.stderr += `Verified: ${verified}.
|
|
32930
|
-
`;
|
|
32931
|
-
output2.verification = verified;
|
|
32932
|
-
}
|
|
32933
33015
|
if (bundle2.selection.fallback && !["prefer-verified", "pinned-selection"].includes(bundle2.selection.replaced?.reason ?? "")) output2.stderr += `Using a previously working revision (${bundle2.selection.replaced?.reason ?? "compatible-fallback"}). Selection recovers automatically when an eligible revision is available.${bundle2.selection.replaced?.until ? ` Protective pause ends ${bundle2.selection.replaced.until}.` : ""} Inspect distribution with ${prefix(`apps history ${appName}`)}.
|
|
32934
33016
|
`;
|
|
32935
33017
|
output2.dispatched = Boolean(dispatchedAt);
|
|
32936
33018
|
saveTimings(timings, performance.now() - commandStarted);
|
|
32937
33019
|
return output2;
|
|
32938
33020
|
}
|
|
33021
|
+
function privateEvidence(value, credentials) {
|
|
33022
|
+
const secrets = new Set(credentials.filter(Boolean));
|
|
33023
|
+
const sensitive2 = /^(?:password|secret|access[_-]?token|refresh[_-]?token|token|authorization|cookie|api[_-]?key)$/iu;
|
|
33024
|
+
JSON.stringify(value, (key, child) => {
|
|
33025
|
+
if (sensitive2.test(key) && typeof child === "string" && child) secrets.add(child);
|
|
33026
|
+
return child;
|
|
33027
|
+
});
|
|
33028
|
+
let redacted = false;
|
|
33029
|
+
const text2 = JSON.stringify(value, (_key, child) => {
|
|
33030
|
+
if (typeof child === "string" && (secrets.has(child) || credentials.some((secret) => secret && child.includes(secret)))) {
|
|
33031
|
+
redacted = true;
|
|
33032
|
+
return "[REDACTED]";
|
|
33033
|
+
}
|
|
33034
|
+
return child;
|
|
33035
|
+
});
|
|
33036
|
+
return { value: JSON.parse(text2), redacted };
|
|
33037
|
+
}
|
|
32939
33038
|
async function executeDraftVerification(input2) {
|
|
32940
33039
|
const { transport } = input2;
|
|
32941
33040
|
const probe = input2.kind === "transport" ? transport.probes[input2.probeOrigin ?? ""] : void 0;
|
|
@@ -32945,9 +33044,9 @@ async function executeDraftVerification(input2) {
|
|
|
32945
33044
|
const schema = input2.kind === "action" ? action.inputs ?? { type: "object", properties: {}, additionalProperties: false } : probe.inputs ?? { type: "object", properties: {}, additionalProperties: false };
|
|
32946
33045
|
let values;
|
|
32947
33046
|
let prepared;
|
|
32948
|
-
const generated = input2.kind === "action" && action && "generated" in action ? Object.fromEntries(Object.entries(action.generated ?? {}).map(([name, spec]) => [name, `${spec.prefix ?? ""}${randomUUID7()}`])) : {};
|
|
33047
|
+
const generated = input2.primaryEvidence?.generated ?? (input2.kind === "action" && action && "generated" in action ? Object.fromEntries(Object.entries(action.generated ?? {}).map(([name, spec]) => [name, `${spec.prefix ?? ""}${randomUUID7()}`])) : {});
|
|
32949
33048
|
try {
|
|
32950
|
-
values = effectiveInputs(schema, input2.sampleInputs, {}, true).values;
|
|
33049
|
+
values = effectiveInputs(schema, input2.primaryEvidence?.inputs ?? input2.sampleInputs, {}, true).values;
|
|
32951
33050
|
const request2 = input2.kind === "action" ? action.request : probe.request;
|
|
32952
33051
|
prepared = prepareRequest(request2, input2.kind === "transport" && input2.probeOrigin ? { ...transport, baseUrl: input2.probeOrigin } : transport, values, generated);
|
|
32953
33052
|
} catch (error62) {
|
|
@@ -32965,11 +33064,12 @@ async function executeDraftVerification(input2) {
|
|
|
32965
33064
|
return { ok: false, message: error62 instanceof Error ? error62.message : "Browser page is unavailable", exitCode: 5, dispatched: false };
|
|
32966
33065
|
}
|
|
32967
33066
|
}
|
|
32968
|
-
let dispatched =
|
|
33067
|
+
let dispatched = Boolean(input2.primaryEvidence);
|
|
32969
33068
|
let dispatchStarted = 0;
|
|
32970
33069
|
let dispatchAt = null;
|
|
32971
33070
|
const browserExecution = {};
|
|
32972
33071
|
let liveBrowser;
|
|
33072
|
+
const knownCredentials = [];
|
|
32973
33073
|
const sendConfigured = async (request2, effect2) => {
|
|
32974
33074
|
const allowNonJsonProbe = input2.kind === "transport" && probe?.response.schema === void 0 && probe?.response.path === void 0;
|
|
32975
33075
|
let credentialDraftId;
|
|
@@ -33009,6 +33109,7 @@ async function executeDraftVerification(input2) {
|
|
|
33009
33109
|
const installation = input2.state.installation();
|
|
33010
33110
|
if (!installation) throw new CredentialError("installation-required", "Installation is no longer linked");
|
|
33011
33111
|
const values2 = await store.read(credential2, { workspaceId: installation.workspaceId, ...credentialDraftId ? { draftId: credentialDraftId } : {}, appId: input2.appId, appTitle: input2.appSlug, transportName: input2.kind === "action" && action && "transport" in action ? action.transport : input2.name, requirements: transport.credentials ?? [], credentialContract: credentialContractKey(transport), destinations: transport.allowedOrigins });
|
|
33112
|
+
knownCredentials.push(...Object.values(values2));
|
|
33012
33113
|
const headers = new Headers(request2.init.headers);
|
|
33013
33114
|
for (const requirement2 of transport.credentials ?? []) headers.set(requirement2.header, `${requirement2.prefix ?? ""}${values2[requirement2.name]}`);
|
|
33014
33115
|
request2 = { ...request2, init: { ...request2.init, headers } };
|
|
@@ -33023,9 +33124,13 @@ async function executeDraftVerification(input2) {
|
|
|
33023
33124
|
return send(input2.fetcher, request2, allowNonJsonProbe);
|
|
33024
33125
|
};
|
|
33025
33126
|
try {
|
|
33026
|
-
const primary = await sendConfigured(prepared, input2.kind === "action" ? action.effect : "read");
|
|
33027
|
-
if (!primary.response.ok) return { ok: false, message: `The app returned HTTP ${primary.response.status}${input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "" : ". Inspect its state before retrying a mutation"}`, exitCode: 4, dispatched };
|
|
33028
|
-
if (input2.kind === "action" && transport.mode === "direct" && dispatchStarted) input2.onDirectBaseline?.(Math.max(0, performance.now() - dispatchStarted), JSON.stringify({ method: prepared.init.method, url: prepared.url.toString(), body: prepared.init.body ?? null }), JSON.stringify(primary.body), dispatchAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
33127
|
+
const primary = input2.primaryEvidence ? { response: new Response(null, { status: input2.primaryEvidence.status }), body: input2.primaryEvidence.body } : await sendConfigured(prepared, input2.kind === "action" ? action.effect : "read");
|
|
33128
|
+
if (!primary.response.ok) return { ok: false, message: `The app returned HTTP ${primary.response.status}${input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "" : ". Inspect its state before retrying a mutation"}`, exitCode: 4, dispatched, clientRejected: primary.response.status >= 400 && primary.response.status < 500 };
|
|
33129
|
+
if (!input2.primaryEvidence && input2.kind === "action" && transport.mode === "direct" && dispatchStarted) input2.onDirectBaseline?.(Math.max(0, performance.now() - dispatchStarted), JSON.stringify({ method: prepared.init.method, url: prepared.url.toString(), body: prepared.init.body ?? null }), JSON.stringify(primary.body), dispatchAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
33130
|
+
if (!input2.primaryEvidence && input2.onPrimary) {
|
|
33131
|
+
const saved = privateEvidence({ status: primary.response.status, body: primary.body, inputs: values, generated }, knownCredentials);
|
|
33132
|
+
input2.onPrimary({ ...saved.value, redacted: saved.redacted });
|
|
33133
|
+
}
|
|
33029
33134
|
if (input2.kind === "transport") {
|
|
33030
33135
|
const selected = probe.response.path !== void 0 ? pointer2(primary.body, probe.response.path) : primary.body;
|
|
33031
33136
|
if (probe.response.schema && !ajv3.compile(probe.response.schema)(selected)) return { ok: false, message: "Probe response does not match its declared schema", exitCode: 12, dispatched };
|
|
@@ -33036,12 +33141,17 @@ async function executeDraftVerification(input2) {
|
|
|
33036
33141
|
if (executable.effect !== "read" && executable.proof) {
|
|
33037
33142
|
const preparedProof = prepareRequest(executable.proof.request, transport, values, generated, value);
|
|
33038
33143
|
const proof = await sendConfigured(preparedProof, "read");
|
|
33039
|
-
|
|
33144
|
+
input2.onReadback?.(privateEvidence(proof.body, knownCredentials).value);
|
|
33145
|
+
try {
|
|
33146
|
+
confirmProof(executable.proof, proof.body, value, values, proof.response.status);
|
|
33147
|
+
} catch (error62) {
|
|
33148
|
+
throw new ResponseContractFailure(error62 instanceof Error ? error62.message : "Readback did not confirm success");
|
|
33149
|
+
}
|
|
33040
33150
|
return { ok: true, output: value, proof: "passed", dispatched: true };
|
|
33041
33151
|
}
|
|
33042
33152
|
return { ok: true, output: value, proof: "not-applicable", dispatched: true };
|
|
33043
33153
|
} catch (error62) {
|
|
33044
|
-
return { ok: false, message: `${(error62 instanceof Error ? error62.message : "The test did not return").replace(/[.\s]+$/u, "")}. ${dispatched ? input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "The read did not return complete evidence" : "The request may have reached the app; inspect its state before another mutation" : "No app request was sent"}`, exitCode: dispatched || error62 instanceof BrowserControlError || ["extension-unavailable", "capture-server-unavailable", "native-host-upgrade-required"].includes(error62?.code ?? "") ? 5 : 2, dispatched };
|
|
33154
|
+
return { ok: false, message: `${(error62 instanceof Error ? error62.message : "The test did not return").replace(/[.\s]+$/u, "")}. ${dispatched ? input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "The read did not return complete evidence" : "The request may have reached the app; inspect its state before another mutation" : "No app request was sent"}`, exitCode: error62 instanceof ResponseContractFailure ? 12 : dispatched || error62 instanceof BrowserControlError || ["extension-unavailable", "capture-server-unavailable", "native-host-upgrade-required"].includes(error62?.code ?? "") ? 5 : 2, dispatched };
|
|
33045
33155
|
} finally {
|
|
33046
33156
|
if (browserExecution.scope) await liveBrowser?.closeScope(browserExecution.scope.id).catch(() => {
|
|
33047
33157
|
});
|
|
@@ -33497,7 +33607,7 @@ Investigate: ${prefix(`apps update ${appName}`)}
|
|
|
33497
33607
|
const flags = inputs.requiredNames.map((name) => ` --${name.replaceAll("_", "-")} <chosen-value>`).join("");
|
|
33498
33608
|
return `${doc.name} \u2014 ${doc.title}
|
|
33499
33609
|
${doc.description}
|
|
33500
|
-
Effect: ${doc.effect ?? "unknown"}. ${doc.transport ? `Connection: ${doc.transport.mode}${doc.transport.credentials.length ? `; saved fields: ${doc.transport.credentials.map((f) => f.name).join(", ")}` : "; no saved credential required"}. ` : ""}${doc.proof ? `
|
|
33610
|
+
Effect: ${doc.effect ?? "unknown"}. ${doc.transport ? `Connection: ${doc.transport.mode}${doc.transport.credentials.length ? `; saved fields: ${doc.transport.credentials.map((f) => f.name).join(", ")}` : "; no saved credential required"}. ` : ""}${doc.proof ? `Discovery tested ${doc.proof}; this run checks the response without automatic readback. Use a published read when the task needs state confirmation.` : ""}
|
|
33501
33611
|
${inputs.required.length ? `Required inputs
|
|
33502
33612
|
${inputs.required.join("\n")}
|
|
33503
33613
|
` : "No required caller inputs.\n"}${inputs.optional.length ? `Optional inputs
|
|
@@ -33872,8 +33982,8 @@ var init_capture_derivation = __esm({
|
|
|
33872
33982
|
});
|
|
33873
33983
|
|
|
33874
33984
|
// src/commands/discover.ts
|
|
33875
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, renameSync as
|
|
33876
|
-
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
33985
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
33986
|
+
import { createHash as createHash5, randomUUID as randomUUID9 } from "node:crypto";
|
|
33877
33987
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
33878
33988
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
33879
33989
|
async function currentResource(api, appId, kind, name) {
|
|
@@ -34028,6 +34138,7 @@ Edit: ${draft.filePath}
|
|
|
34028
34138
|
Publish entity ${draft.name.split(".")[0]} and the selected connection before this action. Fill the action meaning, effect, public input descriptions and their value sources. Select a published connection under dependencies.transport; put representative values in verification.inputs. For browser discovery, declare the intended operation before recording, then capture selection will fill observed structure.
|
|
34029
34139
|
|
|
34030
34140
|
${local}When ready to test the configured request: ${command4(invocation, "discover run")}
|
|
34141
|
+
For a mutation already sent, repair its response/proof and check only readback: ${command4(invocation, "discover run --proof-only")}
|
|
34031
34142
|
For browser observation: ${command4(invocation, "discover capture start")}
|
|
34032
34143
|
`;
|
|
34033
34144
|
if (draft.kind === "transport") {
|
|
@@ -34211,26 +34322,32 @@ async function handleDiscover(invocation, api, state, session, fetcher, root, in
|
|
|
34211
34322
|
} catch (error62) {
|
|
34212
34323
|
if (!(error62 instanceof ApiClientError && error62.status === 404)) throw error62;
|
|
34213
34324
|
}
|
|
34325
|
+
let current;
|
|
34214
34326
|
try {
|
|
34215
|
-
store.start(session.id, slug, null, slug);
|
|
34327
|
+
current = store.start(session.id, slug, null, slug);
|
|
34216
34328
|
} catch (error62) {
|
|
34217
34329
|
return fail5(`${error62 instanceof Error ? error62.message : "Could not start discovery"}.
|
|
34330
|
+
Retry: ${command4(invocation, `apps new ${slug}`)}
|
|
34218
34331
|
`);
|
|
34219
34332
|
}
|
|
34220
34333
|
withBaselines(root, (baselines) => baselines.startPreparation(session.id, slug));
|
|
34221
|
-
|
|
34334
|
+
const opened = await openDraft(invocation, api, store, session, root, "app");
|
|
34335
|
+
if (current.archivedDrafts) opened.stdout = `Drafts for the previous app were archived: ${current.archivedDrafts}
|
|
34336
|
+
${opened.stdout}`;
|
|
34337
|
+
return opened;
|
|
34222
34338
|
}
|
|
34223
34339
|
if (invocation.key === "appsUpdate") {
|
|
34224
34340
|
const app2 = await api.call("getApp", { params: { app: invocation.positionals[0] } });
|
|
34225
34341
|
const selected = store.selected(session.id);
|
|
34226
|
-
if (selected) return fail5(`This agent is already editing ${selected.kind} ${selected.name} for ${selected.appSlug}. Its draft is preserved. Finish, defer or discard it before opening another app. Inspect ${command4(invocation, "discover status")}.
|
|
34342
|
+
if (selected && (selected.appSlug !== app2.slug || selected.appId === app2.id)) return fail5(`This agent is already editing ${selected.kind} ${selected.name} for ${selected.appSlug}. Its draft is preserved. Finish, defer or discard it before opening another app. Inspect ${command4(invocation, "discover status")}.
|
|
34227
34343
|
`, 8);
|
|
34228
|
-
store.start(session.id, app2.slug, app2.id, app2.title);
|
|
34344
|
+
const current = store.start(session.id, app2.slug, app2.id, app2.title);
|
|
34229
34345
|
withBaselines(root, (baselines) => baselines.startPreparation(session.id, app2.slug));
|
|
34230
34346
|
const accessGuidance = app2.transports.some((item) => item.mode === "direct" && Array.isArray(item.credentials) && item.credentials.length) ? `
|
|
34231
34347
|
For another user's key, inspect the published connection first: ${command4(invocation, `transports show ${app2.slug}`)}. If its issuance steps or permissions are missing, research the app's official access documentation, then revise that same connection with ${command4(invocation, "discover draft transport <published-name>")}. Keep its mode, destination, credential fields and probe unless the official contract changed. Draft an action only when the requested operation itself is missing.
|
|
34232
34348
|
` : "";
|
|
34233
|
-
return ok4(
|
|
34349
|
+
return ok4(`${current.archivedDrafts ? `Drafts for the previous app were archived: ${current.archivedDrafts}
|
|
34350
|
+
` : ""}Opened ${app2.title} for changes. No draft is selected.
|
|
34234
34351
|
${accessGuidance}First check whether the requested operation exists: ${command4(invocation, `actions search ${app2.slug} "<keyword>"`)}
|
|
34235
34352
|
|
|
34236
34353
|
Create or edit one resource. ${actionNameHelp}
|
|
@@ -34310,7 +34427,7 @@ Keep for later: ${command4(invocation, "discover defer")}
|
|
|
34310
34427
|
Abandon unpublished edits: ${command4(invocation, "discover discard")}
|
|
34311
34428
|
`);
|
|
34312
34429
|
}
|
|
34313
|
-
const retained = store.retained(current.appSlug);
|
|
34430
|
+
const retained = store.retained(current.appSlug).filter((draft2) => draft2.appId === current.appId);
|
|
34314
34431
|
return ok4(`Editing ${current.title}. No draft is selected.
|
|
34315
34432
|
${retained.length ? `Retained files:
|
|
34316
34433
|
${retained.map((item) => ` ${item.kind} ${item.name}: ${item.filePath}
|
|
@@ -34430,7 +34547,8 @@ async function verificationIdentity(api, checked) {
|
|
|
34430
34547
|
}
|
|
34431
34548
|
const closure = await closureHash(executable, deps);
|
|
34432
34549
|
const fingerprint3 = await hashJson({ executable, closure, inputs: checked.envelope?.verification?.inputs ?? {} });
|
|
34433
|
-
|
|
34550
|
+
const primary = await hashJson({ appId: checked.draft.appId, name: checked.draft.name, effect: "effect" in action ? action.effect : null, request: "request" in action ? action.request : null, inputs: "inputs" in action ? action.inputs ? semanticSchema(action.inputs) : null : null, generated: "generated" in action ? action.generated ?? null : null, samples: checked.envelope?.verification?.inputs ?? {}, deps });
|
|
34551
|
+
return { executable, closure, fingerprint: fingerprint3, primary };
|
|
34434
34552
|
}
|
|
34435
34553
|
const fingerprint2 = await hashJson({ executable, inputs: checked.envelope?.verification?.inputs ?? {} });
|
|
34436
34554
|
return { executable, fingerprint: fingerprint2 };
|
|
@@ -34485,6 +34603,8 @@ File: ${draft.filePath}
|
|
|
34485
34603
|
if (parsed.success && error62.status < 500) {
|
|
34486
34604
|
store.clearPublication(draft.id);
|
|
34487
34605
|
const issue2 = parsed.data.error;
|
|
34606
|
+
if (issue2.code === "app-not-found") return fail5(`${issue2.code} \u2014 The published app is no longer accessible. Recheck its current registry identity with ${command4(invocation, `apps new ${draft.appSlug}`)}. Local drafts are preserved.
|
|
34607
|
+
`, cliExitForError(issue2), true);
|
|
34488
34608
|
if (issue2.code === "version-conflict") return fail5(`The published resource changed after this draft was opened. Your edits are retained. Reconcile with ${command4(invocation, "discover rebase")}. No new version was published.
|
|
34489
34609
|
`, 8, true);
|
|
34490
34610
|
if (removing && issue2.code === "dependents-exist" && Array.isArray(issue2.details.dependents)) {
|
|
@@ -34577,7 +34697,7 @@ async function rebaseDraft(invocation, api, store, session) {
|
|
|
34577
34697
|
const updated = applyEdits(source, modify(source, ["expectedVersion"], current.version, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
|
|
34578
34698
|
const temporary = `${draft.filePath}.${randomUUID9()}.tmp`;
|
|
34579
34699
|
writeFileSync8(temporary, updated, { flag: "wx", mode: 384 });
|
|
34580
|
-
|
|
34700
|
+
renameSync3(temporary, draft.filePath);
|
|
34581
34701
|
store.setBaseVersion(draft.id, current.version);
|
|
34582
34702
|
return ok4(`The published ${label(draft.kind)} ${draft.name} changed. The removal now targets version ${current.version}.
|
|
34583
34703
|
Review the current definition: ${comparisonPath}
|
|
@@ -34618,28 +34738,59 @@ async function verifyDraft(invocation, api, state, store, session, fetcher, root
|
|
|
34618
34738
|
const installation = state.installation();
|
|
34619
34739
|
if (installation) await api.call("acquireSession", { params: { id: apiSessionId(installation.id, session) }, body: { purpose: "discovery", appId: draft.appId } });
|
|
34620
34740
|
const write = Boolean(action && "effect" in action && action.effect !== "read");
|
|
34621
|
-
const
|
|
34622
|
-
if (
|
|
34623
|
-
|
|
34624
|
-
|
|
34741
|
+
const proofOnly = invocation.flags.includes("proof-only");
|
|
34742
|
+
if (proofOnly && !write) return fail5("--proof-only requires a mutation draft with a saved successful primary response. No app request was sent.\n");
|
|
34743
|
+
const primaryFingerprint = await hashJson({ primary: "primary" in identity2 ? identity2.primary : identity2.fingerprint, browserTab: transport.mode === "browser" ? envelope.capture?.tab || state.browserTab(session.id, draft.appId) || null : null, credential: state.useSession(session.id, draft.appId)?.credentials[action && "transport" in action ? action.transport : draft.name] ?? null });
|
|
34744
|
+
const previous = store.requestAttempt(draft.id, primaryFingerprint);
|
|
34745
|
+
const legacy = store.attempt(draft.id);
|
|
34746
|
+
const proofCommand = command4(invocation, "discover run --proof-only");
|
|
34747
|
+
const evidence = store.evidence(draft.id);
|
|
34748
|
+
if (write && !proofOnly && evidence?.fingerprint === identity2.fingerprint && (previous?.state === "success" || legacy?.fingerprint === identity2.fingerprint && legacy.state === "success")) return ok4(`This configured mutation was already tested successfully. Saved output: ${evidence.resultPath}
|
|
34625
34749
|
Review it and publish; no app request was sent.
|
|
34626
|
-
`)
|
|
34750
|
+
`);
|
|
34751
|
+
if (write && !proofOnly && (previous && previous.state !== "client-rejected" || !previous && legacy && legacy.state !== "client-rejected")) return fail5(`This mutation was already sent or may have reached the app. Do not repeat it. ${previous?.responsePath ? `Inspect the saved response: ${previous.responsePath}. After repairing response/proof, check without repeating the write: ${proofCommand}` : "No reusable response is available; inspect the app through an authorized read before choosing another test."}
|
|
34627
34752
|
`, 12);
|
|
34753
|
+
let primaryEvidence;
|
|
34754
|
+
if (proofOnly) {
|
|
34755
|
+
if (!previous?.responsePath) return fail5("No saved primary response matches this request, inputs and connection. No app request was sent. Restore the original test definition or inspect the app through a read; do not repeat an uncertain write.\n", 12);
|
|
34756
|
+
try {
|
|
34757
|
+
const saved = readFileSync8(previous.responsePath, "utf8");
|
|
34758
|
+
if (createHash5("sha256").update(saved).digest("hex") !== previous.responseDigest) throw new Error("Saved response changed");
|
|
34759
|
+
primaryEvidence = JSON.parse(saved);
|
|
34760
|
+
if (primaryEvidence.redacted) return fail5("Saved evidence contains redacted credentials and cannot be reused as proof. No app request was sent; inspect the intended state through an authorized read.\n", 12);
|
|
34761
|
+
} catch {
|
|
34762
|
+
return fail5("The saved primary response is unavailable. No app request was sent; inspect the app through a read.\n", 12);
|
|
34763
|
+
}
|
|
34628
34764
|
}
|
|
34629
|
-
|
|
34630
|
-
|
|
34631
|
-
|
|
34632
|
-
|
|
34765
|
+
let responsePath = previous?.responsePath;
|
|
34766
|
+
let readbackPath;
|
|
34767
|
+
const savePrivate = (value, name) => {
|
|
34768
|
+
const directory = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
|
|
34769
|
+
const path2 = join12(directory, name);
|
|
34770
|
+
writeFileSync8(path2, `${JSON.stringify(value, null, 2)}
|
|
34771
|
+
`, { flag: "wx", mode: 384 });
|
|
34772
|
+
return path2;
|
|
34773
|
+
};
|
|
34774
|
+
if (write && !proofOnly) store.saveRequestAttempt(draft.id, primaryFingerprint, "possibly-dispatched");
|
|
34775
|
+
const executed = await executeDraftVerification({ api, state, session, root, fetcher, appId: draft.appId, appSlug: draft.appSlug, kind: draft.kind, name: draft.name, action, transport, commandPrefix: command4(invocation, "").trim(), browserTab: envelope.capture?.tab || void 0, probeOrigin: envelope.verification?.probeOrigin, sampleInputs: envelope.verification?.inputs ?? {}, primaryEvidence, onPrimary: (saved) => {
|
|
34776
|
+
responsePath = savePrivate(saved, "primary.json");
|
|
34777
|
+
store.saveRequestAttempt(draft.id, primaryFingerprint, "possibly-dispatched", responsePath, createHash5("sha256").update(readFileSync8(responsePath)).digest("hex"));
|
|
34778
|
+
}, onReadback: (body) => {
|
|
34779
|
+
readbackPath = savePrivate(body, "readback.json");
|
|
34780
|
+
}, onDispatch: () => withBaselines(root, (baselines) => baselines.closeObservation(draft.id, "dispatch")), onDirectBaseline: (durationMs, request2, response, startedAt, finishedAt) => {
|
|
34633
34781
|
if (draft.kind === "action") withBaselines(root, (baselines) => baselines.recordDirect({ draftId: draft.id, appId: draft.appId, sessionId: session.id, durationMs, request: request2, response, invocationId, startedAt, finishedAt }));
|
|
34634
34782
|
} });
|
|
34635
34783
|
if (!executed.ok) {
|
|
34636
|
-
if (write && !executed.dispatched) store.
|
|
34784
|
+
if (write && !proofOnly && (!executed.dispatched || executed.clientRejected)) store.saveRequestAttempt(draft.id, primaryFingerprint, "client-rejected");
|
|
34637
34785
|
return fail5(`discover-run-failed \u2014 ${executed.message}.
|
|
34638
34786
|
File: ${draft.filePath}
|
|
34639
|
-
|
|
34787
|
+
${responsePath ? `Saved primary response and test inputs: ${responsePath}
|
|
34788
|
+
` : ""}${readbackPath ? `Saved readback: ${readbackPath}
|
|
34789
|
+
` : ""}${write && responsePath ? `Repair the response/proof, then check without repeating the write: ${proofCommand}
|
|
34790
|
+
` : ""}`, executed.exitCode, executed.dispatched);
|
|
34640
34791
|
}
|
|
34641
34792
|
withBaselines(root, (baselines) => baselines.finishPreparation(session.id, draft.appSlug, draft.appId, transport.mode));
|
|
34642
|
-
if (write) store.
|
|
34793
|
+
if (write) store.saveRequestAttempt(draft.id, primaryFingerprint, "success");
|
|
34643
34794
|
const dir = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
|
|
34644
34795
|
const resultPath = join12(dir, "verification.json");
|
|
34645
34796
|
writeFileSync8(resultPath, `${JSON.stringify(executed.output, null, 2)}
|
|
@@ -34647,8 +34798,10 @@ File: ${draft.filePath}
|
|
|
34647
34798
|
const prior = store.evidence(draft.id);
|
|
34648
34799
|
const probes = draft.kind === "transport" ? [...((prior?.fingerprint === identity2.fingerprint ? prior.data.probes : []) ?? []).filter((item) => item.origin !== envelope.verification?.probeOrigin), { origin: envelope.verification.probeOrigin, verifiedAt: (/* @__PURE__ */ new Date()).toISOString() }] : void 0;
|
|
34649
34800
|
store.saveEvidence(draft.id, identity2.fingerprint, resultPath, probes ? { probes } : { proof: executed.proof });
|
|
34801
|
+
const source = readFileSync8(draft.filePath, "utf8");
|
|
34802
|
+
writeFileSync8(draft.filePath, applyEdits(source, modify(source, ["verification", "review"], "pending", { formattingOptions: { insertSpaces: true, tabSize: 2 } })), { mode: 384 });
|
|
34650
34803
|
return { stdout: `${JSON.stringify(executed.output, null, 2)}
|
|
34651
|
-
`, stderr: `Executed the configured ${draft.kind === "transport" ? "probe" : "action"} ${draft.name} and checked its response contract.
|
|
34804
|
+
`, stderr: `${proofOnly ? "Checked the saved primary response and executed only its readback" : `Executed the configured ${draft.kind === "transport" ? "probe" : "action"} ${draft.name} and checked its response contract`}.${write ? " Discovery readback confirmed the intended state." : ""}
|
|
34652
34805
|
Saved output: ${resultPath}
|
|
34653
34806
|
|
|
34654
34807
|
Review the actual objects and field meanings against the official docs or UI.
|
|
@@ -35207,6 +35360,8 @@ async function finalizeOutput(output2, sink, directory = tmpdir3()) {
|
|
|
35207
35360
|
if (size() > displayBudget) {
|
|
35208
35361
|
throw new Error("output notices exceed display budget");
|
|
35209
35362
|
}
|
|
35363
|
+
const guidance = "Search the saved file for the needed identifier or name, then extract matching records with jq or focused file reads. Do not load the entire file into context; it contains only fetched data.\n";
|
|
35364
|
+
if (size() + Buffer.byteLength(guidance) <= displayBudget) stderr += guidance;
|
|
35210
35365
|
}
|
|
35211
35366
|
if (stdout) sink.stdout(stdout);
|
|
35212
35367
|
if (stderr) sink.stderr(stderr);
|
|
@@ -35229,7 +35384,7 @@ var init_output = __esm({
|
|
|
35229
35384
|
// ../../node_modules/.pnpm/tinyexec@1.3.1/node_modules/tinyexec/dist/main.mjs
|
|
35230
35385
|
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
35231
35386
|
import { cwd } from "node:process";
|
|
35232
|
-
import { basename, delimiter, dirname as dirname7, normalize, resolve as resolve6 } from "node:path";
|
|
35387
|
+
import { basename as basename2, delimiter, dirname as dirname7, normalize, resolve as resolve6 } from "node:path";
|
|
35233
35388
|
import { fileURLToPath } from "node:url";
|
|
35234
35389
|
import { pipeline } from "node:stream/promises";
|
|
35235
35390
|
import { PassThrough } from "node:stream";
|
|
@@ -35299,7 +35454,7 @@ function normalizeSpawnCommand(command6, args = [], options2 = {}) {
|
|
|
35299
35454
|
const separatorIndex = line.indexOf(" ");
|
|
35300
35455
|
const path2 = separatorIndex !== -1 ? line.slice(0, separatorIndex) : line;
|
|
35301
35456
|
const argument = separatorIndex !== -1 ? line.slice(separatorIndex + 1) : "";
|
|
35302
|
-
const binary =
|
|
35457
|
+
const binary = basename2(path2);
|
|
35303
35458
|
shebang = binary === "env" ? argument || null : binary;
|
|
35304
35459
|
}
|
|
35305
35460
|
}
|
|
@@ -35627,8 +35782,8 @@ var init_main2 = __esm({
|
|
|
35627
35782
|
});
|
|
35628
35783
|
|
|
35629
35784
|
// src/plugin.ts
|
|
35630
|
-
import { createHash as
|
|
35631
|
-
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync, renameSync as
|
|
35785
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
35786
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
|
|
35632
35787
|
import { homedir as homedir2 } from "node:os";
|
|
35633
35788
|
import { dirname as dirname8, join as join14, resolve as resolve7 } from "node:path";
|
|
35634
35789
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -35645,7 +35800,7 @@ function files(root, relative = "") {
|
|
|
35645
35800
|
}).sort();
|
|
35646
35801
|
}
|
|
35647
35802
|
function digest(root) {
|
|
35648
|
-
const hash4 =
|
|
35803
|
+
const hash4 = createHash6("sha256");
|
|
35649
35804
|
for (const path2 of files(root)) hash4.update(path2).update("\0").update(readFileSync9(join14(root, path2))).update("\0");
|
|
35650
35805
|
return hash4.digest("hex");
|
|
35651
35806
|
}
|
|
@@ -35740,11 +35895,11 @@ function ensureAgentPlugin(agent2, env, sourceOverride) {
|
|
|
35740
35895
|
writeJson(manifestPath, manifest);
|
|
35741
35896
|
writeJson(join14(staging, markerName), { schema: "apilift.plugin-install/v1", package: "apilift", version: version2, bundleDigest });
|
|
35742
35897
|
const backup = `${plugin}.backup-${process.pid}`;
|
|
35743
|
-
if (existsSync5(plugin))
|
|
35898
|
+
if (existsSync5(plugin)) renameSync4(plugin, backup);
|
|
35744
35899
|
try {
|
|
35745
|
-
|
|
35900
|
+
renameSync4(staging, plugin);
|
|
35746
35901
|
} catch (error62) {
|
|
35747
|
-
if (existsSync5(backup))
|
|
35902
|
+
if (existsSync5(backup)) renameSync4(backup, plugin);
|
|
35748
35903
|
throw error62;
|
|
35749
35904
|
}
|
|
35750
35905
|
rmSync4(backup, { recursive: true, force: true });
|
|
@@ -35974,8 +36129,8 @@ var init_analytics2 = __esm({
|
|
|
35974
36129
|
});
|
|
35975
36130
|
|
|
35976
36131
|
// src/usage/ownership.ts
|
|
35977
|
-
import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as
|
|
35978
|
-
import { createHash as
|
|
36132
|
+
import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync5, unlinkSync as unlinkSync4, statSync as statSync3 } from "node:fs";
|
|
36133
|
+
import { createHash as createHash7, randomUUID as randomUUID12 } from "node:crypto";
|
|
35979
36134
|
import { join as join15, dirname as dirname9, isAbsolute as isAbsolute2 } from "node:path";
|
|
35980
36135
|
import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
|
|
35981
36136
|
function requestCollectorTarget(root, entry2, env) {
|
|
@@ -35985,7 +36140,7 @@ function requestCollectorTarget(root, entry2, env) {
|
|
|
35985
36140
|
try {
|
|
35986
36141
|
const target = { owner: owner.id, entry: entry2, interpreter: process.execPath, fingerprint: sourceFingerprint(entry2), env: Object.fromEntries(runtimeKeys.map((key) => [key, env[key] ?? null])) };
|
|
35987
36142
|
writeFileSync10(pending, JSON.stringify(target), { mode: 384 });
|
|
35988
|
-
|
|
36143
|
+
renameSync5(pending, targetFile(root));
|
|
35989
36144
|
} finally {
|
|
35990
36145
|
try {
|
|
35991
36146
|
unlinkSync4(pending);
|
|
@@ -36031,7 +36186,7 @@ var init_ownership = __esm({
|
|
|
36031
36186
|
"use strict";
|
|
36032
36187
|
ownerLifetimeMs = 6e4;
|
|
36033
36188
|
targetFile = (root) => join15(root, "usage", "collector-target.json");
|
|
36034
|
-
sourceFingerprint = (entry2) =>
|
|
36189
|
+
sourceFingerprint = (entry2) => createHash7("sha256").update(readFileSync10(entry2)).digest("hex");
|
|
36035
36190
|
runtimeKeys = ["APILIFT_URL", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "APILIFT_BIN", "APILIFT_DEV_PLUGIN_ROOT"];
|
|
36036
36191
|
ownerFile = (root) => join15(root, "usage", "collector-owner.json");
|
|
36037
36192
|
}
|
|
@@ -38102,7 +38257,7 @@ var init_compat2 = __esm({
|
|
|
38102
38257
|
});
|
|
38103
38258
|
|
|
38104
38259
|
// src/native-host/registration.ts
|
|
38105
|
-
import { createHash as
|
|
38260
|
+
import { createHash as createHash8, randomUUID as randomUUID13 } from "node:crypto";
|
|
38106
38261
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
38107
38262
|
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "node:fs";
|
|
38108
38263
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
@@ -38249,7 +38404,7 @@ var fingerprint, hostNames, validId, posixQuote, absoluteEnv;
|
|
|
38249
38404
|
var init_registration = __esm({
|
|
38250
38405
|
"src/native-host/registration.ts"() {
|
|
38251
38406
|
"use strict";
|
|
38252
|
-
fingerprint = (bytes) =>
|
|
38407
|
+
fingerprint = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
38253
38408
|
hostNames = /* @__PURE__ */ new Set(["com.apilift.browser", "com.apilift.browser.dev"]);
|
|
38254
38409
|
validId = /^[a-p]{32}$/;
|
|
38255
38410
|
posixQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
@@ -38422,7 +38577,7 @@ var init_update = __esm({
|
|
|
38422
38577
|
});
|
|
38423
38578
|
|
|
38424
38579
|
// src/setup-check.ts
|
|
38425
|
-
import { createHash as
|
|
38580
|
+
import { createHash as createHash9, randomUUID as randomUUID14 } from "node:crypto";
|
|
38426
38581
|
import { existsSync as existsSync8, readFileSync as readFileSync12, realpathSync as realpathSync3, writeFileSync as writeFileSync13 } from "node:fs";
|
|
38427
38582
|
import { dirname as dirname12, join as join18 } from "node:path";
|
|
38428
38583
|
async function setupCheckBody(client, scopeId, value) {
|
|
@@ -38432,7 +38587,7 @@ async function setupCheckBody(client, scopeId, value) {
|
|
|
38432
38587
|
if (!artifact || typeof artifact.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(artifact.sha256) || !Number.isInteger(artifact.bytes) || Number(artifact.bytes) < 1 || Number(artifact.bytes) > maxCheckBodyBytes || artifact.encoding !== "utf-8") return null;
|
|
38433
38588
|
try {
|
|
38434
38589
|
const bytes = await client.blob(scopeId, artifact.sha256);
|
|
38435
|
-
if (!bytes || bytes.length !== artifact.bytes ||
|
|
38590
|
+
if (!bytes || bytes.length !== artifact.bytes || createHash9("sha256").update(bytes).digest("hex") !== artifact.sha256) return null;
|
|
38436
38591
|
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
38437
38592
|
} catch {
|
|
38438
38593
|
return null;
|
|
@@ -17,10 +17,10 @@ For every requested action, assess current first-party operation and access docu
|
|
|
17
17
|
|
|
18
18
|
Use `apilift discover draft <kind> [name]` for `app`, `entity`, `transport`, `browser-page`, or `action`. Its response gives a private JSONC path; edit that file. The file contains the published `definition` and local `dependencies`, `verification`, and `capture` settings. App origin is its primary UI location, not every request destination. Publish missing entity and connection resources before an action that depends on them; a browser connection also needs a published browser page. Keep credential values and recurring local context values out of every definition. Read [operation-contract.md](references/operation-contract.md) before defining an action.
|
|
19
19
|
|
|
20
|
-
Check the selected file with `apilift discover check`; it sends no app request and reports independent field errors. An executable connection or action needs an explicit `apilift discover run` with representative values in `verification.inputs`; a connection also selects `verification.probeOrigin`. Inspect saved output against official documentation or observed app state and set `verification.review` to `accepted` only when its meaning is correct. A schema match or HTTP success alone is insufficient. For a mutation, assess its separate readback proof and never repeat the write merely to improve evidence. Metadata resources need no app execution. `apilift discover publish` publishes the exact selected version after its gates and sends no app request.
|
|
20
|
+
Check the selected file with `apilift discover check`; it sends no app request and reports independent field errors. An executable connection or action needs an explicit `apilift discover run` with representative values in `verification.inputs`; a connection also selects `verification.probeOrigin`. Inspect saved output against official documentation or observed app state and set `verification.review` to `accepted` only when its meaning is correct. A schema match or HTTP success alone is insufficient. For a mutation, assess its separate readback proof and never repeat the write merely to improve evidence. After repairing a failed response or proof, `apilift discover run --proof-only` checks the saved response and sends only the readback. Metadata resources need no app execution. `apilift discover publish` publishes the exact selected version after its gates and sends no app request.
|
|
21
21
|
|
|
22
22
|
## Recover and finish
|
|
23
23
|
|
|
24
24
|
Use `apilift discover status` to recover the selected draft, result path, recording phase, or pending publication. A lost publication response is reconciled with the exact retained `discover publish` request before editing, deferring, or discarding. `discover rebase` is for a version conflict after reviewing the current resource. `discover defer` retains owned work, `discover discard` abandons only its unpublished draft, and `discover close` leaves the app when owned in-flight work has settled. A stopped recording must be confirmed or canceled before changing drafts or tabs. Follow [recovery.md](references/recovery.md) before treating a failed route as a terminal blocker.
|
|
25
25
|
|
|
26
|
-
Completion means the requested actions are published and the original authorized outcome is handled, or a specific human-only or external prerequisite is identified. Review consumer descriptions for behavior, consequences and limits:
|
|
26
|
+
Completion means the requested actions are published and the original authorized outcome is handled, or a specific human-only or external prerequisite is identified. Review consumer descriptions for behavior, consequences and limits: author verification establishes publication evidence, while published execution validates its response without automatic readback. Do not prescribe routine browser inspection. Consumers decide whether their task needs additional verification. Inspect published consumer help from a fresh session and obtain required values through its published source actions; the author's local verification inputs are not consumer coverage. Do not send an extra mutation merely to demonstrate publication. Report actual verification, authorized test state, one optional cleanup attempt if relevant, and remaining gaps.
|
|
@@ -4,13 +4,17 @@ Name a precise domain `entity.verb` for the requested outcome. Before transport
|
|
|
4
4
|
|
|
5
5
|
An action's published `definition` describes its effect, JSON Schema `inputs`, request, response and, for a mutation, readback proof. `dependencies.transport` selects one published connection and is translated to the action's selected transport at publication. Request fields include method, URL, optional headers, query and JSON body. Use typed `{"$input":"name"}` and encoded `{input.name}` bindings for caller values. Public protocol literals are allowed; captured account identifiers, credential values and local defaults are not. Use generated bindings for fresh request identifiers, never a caller flag invented from a captured identifier.
|
|
6
6
|
|
|
7
|
-
Public action descriptions state behavior, consequences and limits. Author verification establishes publication evidence;
|
|
7
|
+
Public action descriptions state behavior, consequences and limits. Author verification establishes publication evidence; published execution validates the response without automatic readback. Consumers decide whether their task needs state confirmation through a published read; do not prescribe routine browser inspection.
|
|
8
8
|
|
|
9
9
|
Each visible input and response field needs a semantic description; caller inputs also need synthetic examples. A value owned by the app needs a published source action with a compatible selected result path. Producers never run implicitly: the consumer runs the documented source and supplies its selected value. Do not use a prose-only dependency, a published context-variable resource, or the author's local verification value as a source. Per-app scalar `context` defaults are a consumer convenience with exact input-name matching, not an action contract or account model. Ambiguous generic names are passed explicitly.
|
|
10
10
|
|
|
11
11
|
Select the useful response subtree with `response.path` when needed and validate it with `response.schema`; projected fields use paths relative to the selected subtree or array member. Preserve meaningful variants and nullable fields observed in real results. If verification reports a mismatch, inspect the selected value and correct its type or projection rather than weakening the contract to pass. Keep identity and relationship fields needed for further discovery.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
During discovery, a mutation needs a separate read-only proof request and meaningful comparisons to the intended state. A proof failure leaves the write possibly applied. Repair the proof and use `apilift discover run --proof-only` against the saved primary response; this sends only the readback. Never repeat a possibly sent write just to get stronger proof. For a delete, an explicit absence proof may establish success. A readback or probe using POST may declare semantic `effect: "read"`; method alone is not the effect.
|
|
14
|
+
|
|
15
|
+
Bind a returned identifier in the proof URL with `{result./id}`, or in its JSON body/query with `{"$result":"/id"}`; result pointers refer to the projected primary result. For a nested collection, use `proof.select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}` and compare `equals` paths relative to its single matching member. The selector must match exactly one item. Check identity and the fields that establish the intended effect, not an entire server-maintained object or a fixed array position. JSON Pointers are literal, not templates. Prefer a targeted detail or search read; absence from a partial list does not prove deletion.
|
|
16
|
+
|
|
17
|
+
When the task needs a few records from a large collection, look for native search, filters or a detail endpoint in documentation or observed UI requests before enumerating the list. Expose observed parameters and verify that they narrow the results. If only a full response is available, search its saved file and extract matching records or fields; do not load the entire catalog into context. Keep stable identity and summary fields in list results; inspect variable configuration only for the selected item. Preserve dynamic configuration as structured JSON with an appropriate schema, rather than enumerating catalog entries or removing validation to pass.
|
|
14
18
|
|
|
15
19
|
Put representative local values in `verification.inputs`, run `apilift discover check`, then explicitly `apilift discover run`. Judge the saved output against documentation or observed UI state. Set `verification.review` to `accepted` only after that judgment and `rejected` with notes otherwise; `discover publish` does not execute the app operation again. After publication, inspect `actions show` as a fresh consumer, obtain every required value through published source actions and finish the originally authorized outcome. Do not send a second mutation merely to test publication.
|
|
16
20
|
|
|
@@ -13,6 +13,6 @@ Use explicit flags for ambiguous values. `apilift context set <name> <scalar> --
|
|
|
13
13
|
|
|
14
14
|
For a direct connection requiring a saved credential, use the exact `credentials list`, `use`, or `add` continuation printed for that app and connection. Selection belongs to this agent session and does not switch browser login. Only the person uses the secure credential form; never ask for a secret in chat, shell arguments, environment variables, files, or a generic dashboard field. `credentials unbound` and `attach` are explicit recovery paths after a registry reset, never an invitation to match a secret by app name or origin. Run published browser actions directly: Apilift manages its browser connection and selected tab internally. Do not run browser preflight or use agent browser tools during covered app work unless the user requests browser work or a CLI failure explicitly requires it. Follow returned CLI tab-selection commands when needed; ask the user to sign in only on the returned authentication recovery. An unusable Chrome connection is a local blocker, not evidence that the app lacks an action.
|
|
15
15
|
|
|
16
|
-
Respect policy, entitlement, effect, cost, and unavailable boundaries before dispatch. A refusal does not authorize direct HTTP or a new interface around it. Do not automatically retry a mutation that may have reached the app, including one whose result
|
|
16
|
+
Respect policy, entitlement, effect, cost, and unavailable boundaries before dispatch. A refusal does not authorize direct HTTP or a new interface around it. Do not automatically retry a mutation that may have reached the app, including one whose result was lost. Follow the retained invocation's recovery instruction and inspect state through a documented CLI read. Published execution checks the response without automatic readback. Decide whether the user's outcome needs state confirmation through a published read; do not routinely inspect the UI. Check returned objects against the requested domain meaning, traverse pagination and containers where needed, and do not claim completion from a title or status code alone. Prefer published search, filter and detail actions when only a few records are needed. Read only the selected result fields needed for the decision; oversized output is saved to the path printed by the CLI. Search that file for relevant names or identifiers and extract matching records with ordinary file tools instead of loading the whole file into context. A saved response contains only fetched data, not unfetched upstream pages.
|
|
17
17
|
|
|
18
18
|
If the app, action, required input source, or result field is absent, tell the discovery owner the user outcome, the precise missing consumer capability, semantic input sources, required result, official documentation supplied by the user, and which published actions must remain intact. One owner should extend the app with the `discover` skill, then the consumer should read the newly published action help and complete the original task. Ask about test-state effects only when they are outside existing authorization. `apilift apps release <app>` ends this use session and clears its local selections and defaults when the work is done.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apilift/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=24.16.0 <27"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "e8a4f6750dc5d2eba58515d30b501f737c2846dc",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
42
|
"url": "git+https://github.com/egor-sergeev/apilift.git",
|