Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5aa93140 | ||
|
|
31bb73768c |
@@ -86,22 +86,8 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
|
||||
|
||||
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
|
||||
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
|
||||
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
|
||||
Direct download for OS/ARCH:
|
||||
https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball)
|
||||
Then rerun this installer.",implemented,verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
|
||||
|
||||
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
|
||||
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
|
||||
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
|
||||
Direct download for OS/ARCH:
|
||||
https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball)
|
||||
Then rerun this installer.",implemented,verified by shell branch inspection
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
|
||||
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
|
||||
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
|
||||
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
|
||||
|
||||
|
+1
-33
@@ -25,38 +25,6 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve the current host's Node.js tarball label pair; a label we cannot map
|
||||
# is kept verbatim so the operator can still read it. Falls back on the raw
|
||||
# `uname` outputs rather than inventing a label the Node.js release index does
|
||||
# not publish, so a mismatch is obvious instead of silently wrong.
|
||||
node_host_labels() {
|
||||
case "$(uname -m)" in
|
||||
arm64|aarch64) node_arch_label="arm64" ;;
|
||||
x86_64|amd64) node_arch_label="x64" ;;
|
||||
*) node_arch_label="$(uname -m)" ;;
|
||||
esac
|
||||
case "$(uname -s)" in
|
||||
Darwin) node_os_label="darwin" ;;
|
||||
Linux) node_os_label="linux" ;;
|
||||
*) node_os_label="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Exit with actionable remediation when the active Node.js is unsupported.
|
||||
# The engine floor stays in sync with package.json#engines.node — change both.
|
||||
fail_node_version() {
|
||||
local observed="$1"
|
||||
node_host_labels
|
||||
printf 'REA installation failed: Node.js %s is unsupported.\n\n' "$observed" >&2
|
||||
printf 'REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n' >&2
|
||||
printf ' nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n' >&2
|
||||
printf ' fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n' >&2
|
||||
printf ' Direct download for %s/%s:\n' "$node_os_label" "$node_arch_label" >&2
|
||||
printf ' https://nodejs.org/dist/latest-v24.x/ (grab the %s-%s tarball)\n' "$node_os_label" "$node_arch_label" >&2
|
||||
printf 'Then rerun this installer.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
|
||||
@@ -110,7 +78,7 @@ main() {
|
||||
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
||||
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
||||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
||||
fail_node_version "$node_version"
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
||||
fi
|
||||
|
||||
if [[ -n "$version" ]]; then
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/**
|
||||
* `requireFunctionDossierOracle` is only used inside `verifyRealHopperFixture`,
|
||||
* which runs against the real Hopper bridge after `build:cached`. Loading it
|
||||
* lazily keeps this module importable from `test:fast` (which docs promise to
|
||||
* run "without build" — see docs/testing.md § test scripts) so that mock-only
|
||||
* callers such as `requireCurrentDocument` and `resolveFixtureProcedure` do
|
||||
* not require `dist/`.
|
||||
*/
|
||||
const loadFunctionDossierOracle = () =>
|
||||
import("../../dist/application/RealHopperAssertions.js").then(
|
||||
(module) => module.requireFunctionDossierOracle,
|
||||
);
|
||||
import { requireFunctionDossierOracle } from "../../dist/application/RealHopperAssertions.js";
|
||||
|
||||
/** Require truthful tri-state permissions from real Hopper memory regions. */
|
||||
export const requireTruthfulMemoryRegions = (segments) => {
|
||||
@@ -99,7 +87,6 @@ export async function verifyRealHopperFixture({
|
||||
oracle,
|
||||
normalizedResult,
|
||||
}) {
|
||||
const requireFunctionDossierOracle = await loadFunctionDossierOracle();
|
||||
const main = await resolveFixtureProcedure(
|
||||
client,
|
||||
options,
|
||||
|
||||
@@ -2,17 +2,10 @@ import { execFile } from "node:child_process";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
/**
|
||||
* `mcp-verifier-results.mjs` reaches into `dist/` at module load, so eager
|
||||
* importing it propagates that build dependency to every consumer in this
|
||||
* file. `verifyCompleteToolCatalog` is the only consumer, so we lazy-load
|
||||
* the module there and keep the other helpers (`exec`, `run`, `json`,
|
||||
* `pathExists`) usable from `test:fast` callers that only need `dist/`-free
|
||||
* utilities — see docs/testing.md § test scripts, which promises `test:fast`
|
||||
* runs "without build".
|
||||
*/
|
||||
const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
|
||||
import {
|
||||
requireMcpLifecycleResult,
|
||||
sortVerifierNames,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Promisified child_process execution helper. */
|
||||
export const exec = promisify(execFile);
|
||||
@@ -42,8 +35,6 @@ export const json = (text) => JSON.parse(text);
|
||||
|
||||
/** Verify schema validity and that the MCP catalog matches session metadata. */
|
||||
export const verifyCompleteToolCatalog = async (client, options) => {
|
||||
const { requireMcpLifecycleResult, sortVerifierNames } =
|
||||
await loadMcpVerifierResults();
|
||||
const listed = await client.listTools(undefined, options);
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
for (const tool of listed.tools) {
|
||||
|
||||
@@ -495,7 +495,7 @@ it("reads a bounded logcat dump with the caller count and buffer", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
logcat:
|
||||
"08-10 13:00:00.000 I/Tag( 123): first\n08-10 13:00:01.000 I/Tag( 123): second\n",
|
||||
"08-10 13:00:00.000 I/Tag( 123): first\\n08-10 13:00:01.000 I/Tag( 123): second\\n",
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
@@ -903,7 +903,7 @@ it("resolves packages by case-insensitive substring", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
"pm:list":
|
||||
"package:/data/app/x/base.apk=com.whatsapp\npackage:/data/app/y/base.apk=com.whatsapp.w4b\npackage:/data/app/z/base.apk=com.example\n",
|
||||
"package:/data/app/x/base.apk=com.whatsapp\\npackage:/data/app/y/base.apk=com.whatsapp.w4b\\npackage:/data/app/z/base.apk=com.example\\n",
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
@@ -1111,11 +1111,10 @@ it("quotes caller text before adb joins the remote shell command", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
shift 3
|
||||
# Model adb's remote command joining, then let the device shell parse the
|
||||
# joined string. Its \`sh\` records the arguments it receives.
|
||||
printf '#!/bin/sh\nprintf "%%s\\\\000" "$@" > "${join(root, "observed")}"\n' > "${join(root, "sh")}"
|
||||
chmod +x "${join(root, "sh")}"
|
||||
PATH="${root}:$PATH" /bin/sh -c "$*"
|
||||
# Model adb's remote command joining, then execute the resulting shell string.
|
||||
printf '#!/bin/sh\nprintf "%%s" "$@" > "${join(root, "observed")}"\n' > "${join(root, "ls")}"
|
||||
chmod +x "${join(root, "ls")}"
|
||||
PATH="${root}:$PATH" sh -c "$*"
|
||||
`,
|
||||
);
|
||||
const value = `/sdcard/a 'quoted'; touch ${join(root, "injected")}`;
|
||||
@@ -1123,8 +1122,7 @@ PATH="${root}:$PATH" /bin/sh -c "$*"
|
||||
operation: "list_adb_directory",
|
||||
input: { serial: "device", device_path: value },
|
||||
});
|
||||
const observed = (await readFile(join(root, "observed"), "utf8")).split("\0");
|
||||
expect(observed.slice(2, 4)).toEqual(["rea-list-directory", value]);
|
||||
expect(await readFile(join(root, "observed"), "utf8")).toContain(value);
|
||||
await expect(readFile(join(root, "injected"))).rejects.toThrow();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
createImmutableEvidenceBundle,
|
||||
describeEvidenceBundleFailure,
|
||||
describeValidationFailure,
|
||||
parseEvidenceBundle,
|
||||
singleEvidenceRecordId,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
@@ -79,6 +82,16 @@ const readBundle = async (
|
||||
try {
|
||||
bundle = parseBundle(loaded.value);
|
||||
} catch (cause: unknown) {
|
||||
// An intact record is the wrong selected file, not damaged bundle content.
|
||||
const evidenceId = singleEvidenceRecordId(loaded.value);
|
||||
if (evidenceId !== undefined)
|
||||
return err(
|
||||
new EvidenceFileError("read", "not-bundle", {
|
||||
cause,
|
||||
path: resolve(path),
|
||||
evidenceId,
|
||||
}),
|
||||
);
|
||||
return err(
|
||||
new EvidenceIntegrityError("Evidence bundle validation failed", {
|
||||
cause,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "./hopperErrors.js";
|
||||
import { ProviderSelectionError } from "./providerSelectionError.js";
|
||||
import { UnknownRegistryError } from "./unknownRegistryError.js";
|
||||
import { singleEvidenceRecordMessage } from "./evidenceBundle.js";
|
||||
import { providerRetryAction } from "./providerOperationHealth.js";
|
||||
import type { AnalysisError, AnalysisErrorTag } from "./analysisErrorBase.js";
|
||||
import type { AnalysisErrorProjection } from "./analysisErrorProjection.js";
|
||||
@@ -148,6 +149,7 @@ export const analysisErrorCategory = (
|
||||
error instanceof EvidenceFileError &&
|
||||
(error.reason === "missing" ||
|
||||
error.reason === "not-file" ||
|
||||
error.reason === "not-bundle" ||
|
||||
error.reason === "exists")
|
||||
)
|
||||
return "invalid_input";
|
||||
@@ -406,6 +408,7 @@ const evidenceFileMessage = ({
|
||||
operation,
|
||||
reason,
|
||||
published,
|
||||
evidenceId,
|
||||
}: EvidenceFileError): string => {
|
||||
if (published !== undefined)
|
||||
return "The output file was published, but staging cleanup remains incomplete.";
|
||||
@@ -415,6 +418,7 @@ const evidenceFileMessage = ({
|
||||
: "Evidence output directory does not exist. Choose an existing directory and try again.";
|
||||
if (reason === "not-file")
|
||||
return "Evidence path does not point to a regular file. Choose a file and try again.";
|
||||
if (reason === "not-bundle") return singleEvidenceRecordMessage(evidenceId);
|
||||
if (reason === "exists")
|
||||
return "Evidence file already exists. Choose another path or allow overwrite.";
|
||||
if (reason === "invalid-json")
|
||||
|
||||
@@ -311,6 +311,9 @@ const artifactStateErrorDetails = (
|
||||
operation: error.operation,
|
||||
reason: error.reason,
|
||||
...(error.path === undefined ? {} : { path: error.path }),
|
||||
...(error.evidenceId === undefined
|
||||
? {}
|
||||
: { evidence_id: error.evidenceId }),
|
||||
...(error.published === undefined
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -212,14 +212,32 @@ export const describeEvidenceBundleFailure = (
|
||||
input: unknown,
|
||||
cause: unknown,
|
||||
): string => {
|
||||
const record = evidenceSchema.safeParse(input);
|
||||
if (record.success)
|
||||
return `Expected an Evidence bundle with records and manifest arrays, but this JSON is one Evidence record (${record.data.evidence_id}). Export a session bundle with the MCP export_evidence_bundle tool and supply that file.`;
|
||||
const recordId = singleEvidenceRecordId(input);
|
||||
if (recordId !== undefined) return singleEvidenceRecordMessage(recordId);
|
||||
return cause instanceof z.ZodError
|
||||
? `Evidence bundle does not match the bundle schema ${describeValidationFailure(cause)}. Supply an unmodified bundle written by export_evidence_bundle or rea evidence-export.`
|
||||
: `Evidence bundle validation failed: ${describeValidationFailure(cause)}. Recreate or re-export the bundle, then try again.`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Identify an authentic single Evidence record supplied where a bundle is
|
||||
* expected. A record whose semantic identifier does not match its content is
|
||||
* damaged, not a mis-selected file, so it remains an integrity failure.
|
||||
*/
|
||||
export const singleEvidenceRecordId = (input: unknown): string | undefined => {
|
||||
try {
|
||||
return parseEvidence(input).evidence_id;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** Explain how to obtain a bundle when one Evidence record was supplied. */
|
||||
export const singleEvidenceRecordMessage = (
|
||||
evidenceId: string | undefined,
|
||||
): string =>
|
||||
`Expected an Evidence bundle with records and manifest arrays, but this JSON is one Evidence record${evidenceId === undefined ? "" : ` (${evidenceId})`}. Export a session bundle with the MCP export_evidence_bundle tool and supply that file.`;
|
||||
|
||||
/** Locate the first schema issue, or keep a thrown constraint message. */
|
||||
export const describeValidationFailure = (cause: unknown): string => {
|
||||
if (!(cause instanceof z.ZodError))
|
||||
|
||||
@@ -50,17 +50,27 @@ export class EvidenceFileError extends AnalysisError {
|
||||
readonly path: string | undefined;
|
||||
/** Present only when a write committed before cleanup failed. */
|
||||
readonly published: PublishedEvidenceFile | undefined;
|
||||
/** The single Evidence record found where a bundle was expected. */
|
||||
readonly evidenceId: string | undefined;
|
||||
|
||||
constructor(
|
||||
readonly operation: "read" | "write",
|
||||
readonly reason: "not-file" | "exists" | "invalid-json" | "missing" | "io",
|
||||
readonly reason:
|
||||
| "not-file"
|
||||
| "not-bundle"
|
||||
| "exists"
|
||||
| "invalid-json"
|
||||
| "missing"
|
||||
| "io",
|
||||
options?: AnalysisErrorOptions & {
|
||||
readonly path?: string;
|
||||
readonly published?: PublishedEvidenceFile;
|
||||
readonly evidenceId?: string;
|
||||
},
|
||||
) {
|
||||
super(`Evidence bundle ${operation} failed: ${reason}`, options);
|
||||
this.path = options?.path;
|
||||
this.published = options?.published;
|
||||
this.evidenceId = options?.evidenceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,13 +439,15 @@ describe("evidence bundle filesystem adapter", () => {
|
||||
const record = bundle().records[0];
|
||||
const single = join(directory, "record.json");
|
||||
await writeFile(single, JSON.stringify(record));
|
||||
// An intact record is the caller's file selection, not damaged content.
|
||||
expect(await readEvidenceBundle(single)).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "EvidenceIntegrityError",
|
||||
userMessage: expect.stringContaining(
|
||||
`this JSON is one Evidence record (${String(record?.evidence_id)})`,
|
||||
),
|
||||
_tag: "EvidenceFileError",
|
||||
operation: "read",
|
||||
reason: "not-bundle",
|
||||
path: single,
|
||||
evidenceId: record?.evidence_id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -477,3 +479,21 @@ describe("evidence bundle filesystem adapter", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence bundle single-record selection", () => {
|
||||
it("keeps a tampered single record an integrity failure", async () => {
|
||||
const directory = await createTestTempDirectory("rea-evidence-");
|
||||
const record = bundle().records[0];
|
||||
// A record whose identifier no longer matches its content is damaged,
|
||||
// not a mis-selected file.
|
||||
const tampered = join(directory, "tampered-record.json");
|
||||
await writeFile(
|
||||
tampered,
|
||||
JSON.stringify({ ...record, operation: "tampered_operation" }),
|
||||
);
|
||||
expect(await readEvidenceBundle(tampered)).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "EvidenceIntegrityError" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,19 +17,6 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// install.sh:fail_node_version emits this recovery block verbatim. The fake
|
||||
// `uname` in createFixture reports `x86_64` / `Linux`, so the host labels
|
||||
// resolve to `linux/x64`.
|
||||
const UNSUPPORTED_NODE_RECOVERY_LINUX_X64 =
|
||||
"REA installation failed: Node.js 20.0.0 is unsupported.\n" +
|
||||
"\n" +
|
||||
"REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n" +
|
||||
" nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n" +
|
||||
" fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n" +
|
||||
" Direct download for linux/x64:\n" +
|
||||
" https://nodejs.org/dist/latest-v24.x/ (grab the linux-x64 tarball)\n" +
|
||||
"Then rerun this installer.\n";
|
||||
|
||||
describe("curl installer scenarios", { timeout: 20_000 }, () => {
|
||||
it("installs only a pinned REA CLI with closed stdin", async () => {
|
||||
const fixture = await createFixture();
|
||||
@@ -100,7 +87,7 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
|
||||
"unsupported Node",
|
||||
["--version", "0.3.0"],
|
||||
{ FAKE_NODE_VERSION: "20.0.0" },
|
||||
UNSUPPORTED_NODE_RECOVERY_LINUX_X64,
|
||||
"REA installation failed: Node.js 20.0.0 is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.\n",
|
||||
],
|
||||
[
|
||||
"unreadable Node version",
|
||||
|
||||
@@ -134,10 +134,17 @@ describe("bundle comparison MCP integration", () => {
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result)).toMatchObject({
|
||||
error: {
|
||||
code: "evidence_integrity_mismatch",
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
message: expect.stringContaining(
|
||||
`this JSON is one Evidence record (${record.evidence_id})`,
|
||||
),
|
||||
details: {
|
||||
operation: "read",
|
||||
reason: "not-bundle",
|
||||
path: recordPath,
|
||||
evidence_id: record.evidence_id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user