mcp-scraper 0.44.1 → 0.44.3
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/README.md +2 -2
- package/dist/bin/api-server.cjs +147 -116
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +17 -25
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/{chunk-TMJVCYDL.js → chunk-2SE4GYCS.js} +18 -26
- package/dist/chunk-2SE4GYCS.js.map +1 -0
- package/dist/chunk-32Z52OE4.js +7 -0
- package/dist/chunk-32Z52OE4.js.map +1 -0
- package/dist/{server-D2Z7BF7S.js → server-JJLFY46K.js} +123 -91
- package/dist/server-JJLFY46K.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-2XGU5GVB.js +0 -7
- package/dist/chunk-2XGU5GVB.js.map +0 -1
- package/dist/chunk-TMJVCYDL.js.map +0 -1
- package/dist/server-D2Z7BF7S.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const PACKAGE_VERSION = '0.44.3'\n"],"mappings":";AAAO,IAAM,kBAAkB;","names":[]}
|
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
renewDirectoryArtifactDownload,
|
|
87
87
|
resolveDeploymentProfile,
|
|
88
88
|
transcribeMediaUrl
|
|
89
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-2SE4GYCS.js";
|
|
90
90
|
import {
|
|
91
91
|
auditImageUrls,
|
|
92
92
|
auditImages,
|
|
@@ -142,7 +142,7 @@ import {
|
|
|
142
142
|
} from "./chunk-3ZUBQQPQ.js";
|
|
143
143
|
import {
|
|
144
144
|
PACKAGE_VERSION
|
|
145
|
-
} from "./chunk-
|
|
145
|
+
} from "./chunk-32Z52OE4.js";
|
|
146
146
|
import {
|
|
147
147
|
abandonExtractSettlement,
|
|
148
148
|
countSuccessfulPages,
|
|
@@ -11972,6 +11972,7 @@ function ensureLocalSourcebookSchema() {
|
|
|
11972
11972
|
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
11973
11973
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
11974
11974
|
)`);
|
|
11975
|
+
await publishEligibleEvidenceBackedLocalSourcebookRevisions(db);
|
|
11975
11976
|
})().catch((error) => {
|
|
11976
11977
|
schemaPromise2 = null;
|
|
11977
11978
|
schemaDb2 = null;
|
|
@@ -11979,6 +11980,19 @@ function ensureLocalSourcebookSchema() {
|
|
|
11979
11980
|
});
|
|
11980
11981
|
return schemaPromise2;
|
|
11981
11982
|
}
|
|
11983
|
+
async function publishEligibleEvidenceBackedLocalSourcebookRevisions(db = getDb()) {
|
|
11984
|
+
const result = await db.execute(`UPDATE local_sourcebook_submissions
|
|
11985
|
+
SET status = 'published', published_revision = draft_revision, updated_at = datetime('now')
|
|
11986
|
+
WHERE status = 'needs_review'
|
|
11987
|
+
AND EXISTS (
|
|
11988
|
+
SELECT 1 FROM local_sourcebook_revisions r
|
|
11989
|
+
WHERE r.submission_id = local_sourcebook_submissions.id
|
|
11990
|
+
AND r.revision = local_sourcebook_submissions.draft_revision
|
|
11991
|
+
AND r.actor_kind = 'system'
|
|
11992
|
+
AND r.actor_id = 'local-sourcebook-worker'
|
|
11993
|
+
)`);
|
|
11994
|
+
return Number(result.rowsAffected ?? 0);
|
|
11995
|
+
}
|
|
11982
11996
|
function rowValue(row, key) {
|
|
11983
11997
|
return row[key];
|
|
11984
11998
|
}
|
|
@@ -12048,23 +12062,12 @@ async function getLocalSourcebookDraft(id, ownerUserId) {
|
|
|
12048
12062
|
const result = await getDb().execute({ sql: `SELECT payload_json FROM local_sourcebook_revisions WHERE submission_id = ? AND revision = ?`, args: [id, submission.draftRevision] });
|
|
12049
12063
|
return result.rows[0] ? parseJson2(result.rows[0].payload_json) : null;
|
|
12050
12064
|
}
|
|
12051
|
-
async function reviseLocalSourcebookDraft(id, ownerUserId, payload, baseRevision) {
|
|
12052
|
-
const submission = await getLocalSourcebookSubmission(id, ownerUserId);
|
|
12053
|
-
if (!submission) return null;
|
|
12054
|
-
if (baseRevision !== void 0 && baseRevision !== submission.draftRevision) {
|
|
12055
|
-
throw new Error(`Draft revision conflict: current revision is ${submission.draftRevision}, but the write targeted ${baseRevision}.`);
|
|
12056
|
-
}
|
|
12057
|
-
const revision = submission.draftRevision + 1;
|
|
12058
|
-
await getDb().batch([
|
|
12059
|
-
{ sql: `INSERT INTO local_sourcebook_revisions (submission_id, revision, payload_json, actor_kind, actor_id) VALUES (?, ?, ?, 'owner', ?)`, args: [id, revision, JSON.stringify(payload), String(ownerUserId)] },
|
|
12060
|
-
{ sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, status = CASE WHEN status = 'published' THEN status ELSE 'needs_review' END, updated_at = datetime('now') WHERE id = ? AND owner_user_id = ?`, args: [revision, id, ownerUserId] }
|
|
12061
|
-
], "write");
|
|
12062
|
-
await event(id, "draft.revised", "owner", String(ownerUserId), { revision });
|
|
12063
|
-
return getLocalSourcebookSubmission(id, ownerUserId);
|
|
12064
|
-
}
|
|
12065
12065
|
async function queueLocalSourcebookRefresh(id, ownerUserId) {
|
|
12066
12066
|
const existing = await getLocalSourcebookSubmission(id, ownerUserId);
|
|
12067
12067
|
if (!existing) return null;
|
|
12068
|
+
if (existing.status === "rejected" || existing.status === "unpublished") {
|
|
12069
|
+
throw new Error(`A ${existing.status} Local Sourcebook listing cannot be refreshed by its owner.`);
|
|
12070
|
+
}
|
|
12068
12071
|
const coverage = { ...existing.coverage, refreshRequestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12069
12072
|
await getDb().execute({ sql: `UPDATE local_sourcebook_submissions SET status = 'queued', coverage_json = ?, acquisition_claimed_at = NULL, acquisition_error = NULL, updated_at = datetime('now') WHERE id = ? AND owner_user_id = ?`, args: [JSON.stringify(coverage), id, ownerUserId] });
|
|
12070
12073
|
await event(id, "enrichment.queued", "owner", String(ownerUserId));
|
|
@@ -12102,12 +12105,13 @@ async function completeLocalSourcebookAcquisition(id, payload, coverage, baseRev
|
|
|
12102
12105
|
WHERE EXISTS (SELECT 1 FROM local_sourcebook_submissions WHERE id = ? AND status = 'enriching' AND draft_revision = ?)`,
|
|
12103
12106
|
args: [id, revision, JSON.stringify(payload), id, expectedRevision]
|
|
12104
12107
|
},
|
|
12105
|
-
{ sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, status = '
|
|
12108
|
+
{ sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, published_revision = ?, status = 'published', coverage_json = ?, acquisition_claimed_at = NULL, acquisition_error = NULL, updated_at = datetime('now') WHERE id = ? AND status = 'enriching' AND draft_revision = ?`, args: [revision, revision, JSON.stringify(coverage), id, expectedRevision] }
|
|
12106
12109
|
], "write");
|
|
12107
12110
|
if (Number(results[0]?.rowsAffected ?? 0) !== 1 || Number(results[1]?.rowsAffected ?? 0) !== 1) {
|
|
12108
12111
|
throw new Error(`Local Sourcebook acquisition revision conflict: revision ${expectedRevision} changed before completion.`);
|
|
12109
12112
|
}
|
|
12110
12113
|
await event(id, "enrichment.completed", "system", "local-sourcebook-worker", { revision, coverage });
|
|
12114
|
+
await event(id, "publication.auto_published", "system", "local-sourcebook-worker", { revision });
|
|
12111
12115
|
return getLocalSourcebookSubmission(id);
|
|
12112
12116
|
}
|
|
12113
12117
|
async function failLocalSourcebookAcquisition(id, message) {
|
|
@@ -12130,6 +12134,15 @@ async function listQueuedLocalSourcebookSubmissionIds(limit = 25) {
|
|
|
12130
12134
|
async function publishLocalSourcebookSubmission(id) {
|
|
12131
12135
|
const submission = await getLocalSourcebookSubmission(id);
|
|
12132
12136
|
if (!submission) return null;
|
|
12137
|
+
const revision = await getDb().execute({
|
|
12138
|
+
sql: `SELECT actor_kind, actor_id FROM local_sourcebook_revisions WHERE submission_id = ? AND revision = ? LIMIT 1`,
|
|
12139
|
+
args: [id, submission.draftRevision]
|
|
12140
|
+
});
|
|
12141
|
+
const actorKind = revision.rows[0] ? String(revision.rows[0].actor_kind) : "";
|
|
12142
|
+
const actorId = revision.rows[0] ? String(revision.rows[0].actor_id) : "";
|
|
12143
|
+
if (actorKind !== "system" || actorId !== "local-sourcebook-worker") {
|
|
12144
|
+
throw new Error("Only a system-compiled Local Sourcebook revision can be published.");
|
|
12145
|
+
}
|
|
12133
12146
|
await getDb().execute({ sql: `UPDATE local_sourcebook_submissions SET status = 'published', published_revision = draft_revision, updated_at = datetime('now') WHERE id = ?`, args: [id] });
|
|
12134
12147
|
await event(id, "publication.published", "admin", "admin", { revision: submission.draftRevision });
|
|
12135
12148
|
return getLocalSourcebookSubmission(id);
|
|
@@ -12146,6 +12159,14 @@ async function listPublicLocalSourcebook(category, state) {
|
|
|
12146
12159
|
const result = await getDb().execute({ sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.state = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.business_name`, args: [category, state] });
|
|
12147
12160
|
return result.rows.map((row) => parseJson2(row.payload_json));
|
|
12148
12161
|
}
|
|
12162
|
+
async function listAllPublicLocalSourcebook(category) {
|
|
12163
|
+
await ensureLocalSourcebookSchema();
|
|
12164
|
+
const result = category ? await getDb().execute({
|
|
12165
|
+
sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.state, s.business_name`,
|
|
12166
|
+
args: [category]
|
|
12167
|
+
}) : await getDb().execute(`SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.category, s.state, s.business_name`);
|
|
12168
|
+
return result.rows.map((row) => parseJson2(row.payload_json));
|
|
12169
|
+
}
|
|
12149
12170
|
async function getPublicLocalSourcebook(category, state, slug2) {
|
|
12150
12171
|
await ensureLocalSourcebookSchema();
|
|
12151
12172
|
const result = await getDb().execute({ sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.state = ? AND s.slug = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') LIMIT 1`, args: [category, state, slug2] });
|
|
@@ -12755,6 +12776,37 @@ var MapsExtractor = class {
|
|
|
12755
12776
|
|
|
12756
12777
|
// src/api/local-sourcebook-compiler.ts
|
|
12757
12778
|
import { createHash as createHash3 } from "crypto";
|
|
12779
|
+
|
|
12780
|
+
// src/api/local-sourcebook-public-urls.ts
|
|
12781
|
+
var DEFAULT_ROOT_DOMAIN = "localsourcebook.com";
|
|
12782
|
+
function rootDomain() {
|
|
12783
|
+
const configured = process.env.LOCAL_SOURCEBOOK_PUBLIC_ROOT_DOMAIN?.trim() || DEFAULT_ROOT_DOMAIN;
|
|
12784
|
+
return configured.replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/.*$/, "").replace(/\.+$/, "").toLowerCase();
|
|
12785
|
+
}
|
|
12786
|
+
function localSourcebookPublicUrls(category, state, slug2) {
|
|
12787
|
+
const categoryUrl = `https://${category}.${rootDomain()}`;
|
|
12788
|
+
const statePath = `/${state.toLowerCase()}`;
|
|
12789
|
+
const profilePath = `${statePath}/${slug2}/`;
|
|
12790
|
+
const reviewsPath = `${statePath}/${slug2}/reviews`;
|
|
12791
|
+
return {
|
|
12792
|
+
categoryUrl,
|
|
12793
|
+
nationwideDirectoryUrl: `${categoryUrl}/directory`,
|
|
12794
|
+
stateUrl: `${categoryUrl}${statePath}`,
|
|
12795
|
+
profileUrl: `${categoryUrl}${profilePath}`,
|
|
12796
|
+
reviewsUrl: `${categoryUrl}${reviewsPath}`,
|
|
12797
|
+
profilePath,
|
|
12798
|
+
reviewsPath
|
|
12799
|
+
};
|
|
12800
|
+
}
|
|
12801
|
+
function localSourcebookPublicationLinks(submission) {
|
|
12802
|
+
return {
|
|
12803
|
+
...localSourcebookPublicUrls(submission.category, submission.state, submission.slug),
|
|
12804
|
+
publicationStatus: submission.status,
|
|
12805
|
+
isLive: submission.status === "published" && submission.publishedRevision !== null
|
|
12806
|
+
};
|
|
12807
|
+
}
|
|
12808
|
+
|
|
12809
|
+
// src/api/local-sourcebook-compiler.ts
|
|
12758
12810
|
var CATEGORY_LABELS = {
|
|
12759
12811
|
home: "Home & property services",
|
|
12760
12812
|
professional: "Professional services",
|
|
@@ -13107,6 +13159,7 @@ function compileLocalSourcebookListing(input) {
|
|
|
13107
13159
|
evidenceIds: mapsEvidenceId ? [mapsEvidenceId] : []
|
|
13108
13160
|
}
|
|
13109
13161
|
];
|
|
13162
|
+
const publicUrls = localSourcebookPublicUrls(input.submission.category, input.submission.state, input.submission.slug);
|
|
13110
13163
|
const listing = {
|
|
13111
13164
|
id: input.submission.id,
|
|
13112
13165
|
publicationRevision: input.submission.draftRevision + 1,
|
|
@@ -13115,6 +13168,14 @@ function compileLocalSourcebookListing(input) {
|
|
|
13115
13168
|
state: input.submission.state.toLowerCase(),
|
|
13116
13169
|
stateName: STATE_NAMES[input.submission.state.toUpperCase()] ?? input.submission.state.toUpperCase(),
|
|
13117
13170
|
slug: input.submission.slug,
|
|
13171
|
+
canonicalPaths: {
|
|
13172
|
+
profile: publicUrls.profilePath,
|
|
13173
|
+
reviews: publicUrls.reviewsPath
|
|
13174
|
+
},
|
|
13175
|
+
canonicalUrls: {
|
|
13176
|
+
profile: publicUrls.profileUrl,
|
|
13177
|
+
reviews: publicUrls.reviewsUrl
|
|
13178
|
+
},
|
|
13118
13179
|
name: maps?.name || input.submission.businessName,
|
|
13119
13180
|
summary,
|
|
13120
13181
|
description,
|
|
@@ -13328,11 +13389,12 @@ async function runLocalSourcebookAcquisition(submission, provider = createDefaul
|
|
|
13328
13389
|
await logRequestEvent({ userId: submission.ownerUserId, source: "local_sourcebook_maps", status: "done", query: submission.businessName, location: submission.state.toUpperCase(), resultCount: maps.reviews.length, result: { submissionId: submission.id, placeUrl: maps.placeUrl, cid: maps.cidDecimal ?? maps.cid, reviewsStatus: maps.reviewsStatus, reviewsRetained: maps.reviews.length, services: maps.services.length, areasServed: maps.areasServed.length } });
|
|
13329
13390
|
} catch (error) {
|
|
13330
13391
|
mapsError = errorMessage2(error);
|
|
13392
|
+
maps = null;
|
|
13331
13393
|
await creditMcIdempotent(submission.ownerUserId, MC_COSTS.maps_place, LedgerOperation.REFUND, `Local Sourcebook failed exact-place acquisition: ${submission.id}`, `${billingCycle}:maps-failure-refund`);
|
|
13332
13394
|
mapsRefunded = true;
|
|
13333
13395
|
await logRequestEvent({ userId: submission.ownerUserId, source: "local_sourcebook_maps", status: "failed", query: submission.businessName, location: submission.state.toUpperCase(), error: mapsError });
|
|
13334
13396
|
}
|
|
13335
|
-
if (!
|
|
13397
|
+
if (!maps) throw new Error(`Exact-place acquisition is required before publication. Maps: ${mapsError || "not available"}. Website: ${siteError || (site ? "captured" : "not available")}.`);
|
|
13336
13398
|
const compiled = compileLocalSourcebookListing({
|
|
13337
13399
|
submission,
|
|
13338
13400
|
site,
|
|
@@ -21255,23 +21317,6 @@ var DirectoryIdentitySchema = z25.object({
|
|
|
21255
21317
|
tags: z25.array(z25.string().trim().min(1).max(60)).max(20).default([]),
|
|
21256
21318
|
idempotencyKey: z25.string().trim().min(8).max(200)
|
|
21257
21319
|
});
|
|
21258
|
-
var DirectoryListingDraftSchema = z25.object({
|
|
21259
|
-
id: z25.string().min(1),
|
|
21260
|
-
category: z25.enum(LOCAL_SOURCEBOOK_CATEGORIES),
|
|
21261
|
-
state: z25.string().trim().length(2),
|
|
21262
|
-
slug: z25.string().trim().min(1),
|
|
21263
|
-
name: z25.string().trim().min(2),
|
|
21264
|
-
summary: z25.string().trim().min(20),
|
|
21265
|
-
description: z25.string().trim().min(20),
|
|
21266
|
-
website: z25.url(),
|
|
21267
|
-
services: z25.array(z25.string()),
|
|
21268
|
-
products: z25.array(z25.string()),
|
|
21269
|
-
serviceAreas: z25.array(z25.string()),
|
|
21270
|
-
media: z25.array(z25.record(z25.string(), z25.unknown())),
|
|
21271
|
-
reviews: z25.array(z25.record(z25.string(), z25.unknown())),
|
|
21272
|
-
evidence: z25.array(z25.record(z25.string(), z25.unknown())),
|
|
21273
|
-
faq: z25.array(z25.object({ question: z25.string().min(3), answer: z25.string().min(12), evidenceIds: z25.array(z25.string()) }))
|
|
21274
|
-
}).passthrough();
|
|
21275
21320
|
function normalizeTag(value) {
|
|
21276
21321
|
return value.toLowerCase().trim().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
21277
21322
|
}
|
|
@@ -21352,18 +21397,22 @@ async function resolveLocalSourcebookTags(candidates) {
|
|
|
21352
21397
|
function getLocalSourcebookContract(category) {
|
|
21353
21398
|
return {
|
|
21354
21399
|
name: "Local Sourcebook listing",
|
|
21355
|
-
version: "
|
|
21400
|
+
version: "local_sourcebook_listing_v2",
|
|
21356
21401
|
purpose: "An owner-scoped, evidence-backed local business record with a separate public profile and complete captured-review archive.",
|
|
21357
21402
|
categories: LOCAL_SOURCEBOOK_CATEGORIES,
|
|
21358
21403
|
selectedCategory: category ?? null,
|
|
21359
21404
|
categoryTag: category ? CATEGORY_TAG[category] : null,
|
|
21360
|
-
canonicalRoutes: {
|
|
21405
|
+
canonicalRoutes: {
|
|
21406
|
+
nationwideDirectory: "https://{category}.localsourcebook.com/directory",
|
|
21407
|
+
profile: "https://{category}.localsourcebook.com/{state}/{business-slug}/",
|
|
21408
|
+
reviews: "https://{category}.localsourcebook.com/{state}/{business-slug}/reviews"
|
|
21409
|
+
},
|
|
21361
21410
|
requiredCreateFields: ["category", "state", "businessName", "websiteUrl", "tags", "idempotencyKey"],
|
|
21362
|
-
|
|
21411
|
+
systemCompiledSections: ["identity", "services/products", "service areas/locations", "genuine media", "reviews and theme counts", "evidence", "answered FAQ", "collection receipts"],
|
|
21363
21412
|
workflow: ["list-local-sourcebook-tags", "get-local-sourcebook-contract", "prepare-local-sourcebook-write", "validate-local-sourcebook-write", "local-sourcebook-capture", "local_sourcebook_submission_status"],
|
|
21364
|
-
ownership: "Only the MCP Scraper account that captures a listing can read or
|
|
21413
|
+
ownership: "Only the MCP Scraper account that captures a listing can read it or request a paid evidence refresh.",
|
|
21365
21414
|
acquisition: { websitePagesMaximum: 100, reviewsMaximum: 500, exactMapsIdentityRequired: true, partialResultsRequireStoppingReasons: true },
|
|
21366
|
-
publication: { automatic: false, adminReviewRequired:
|
|
21415
|
+
publication: { automatic: true, evidenceCompiledOnly: true, ownerAuthoredClaimsAccepted: false, adminReviewRequired: false, lastPublishedRevisionRemainsPublicDuringRefresh: true },
|
|
21367
21416
|
tagPolicy: { inspectVocabularyFirst: true, minimum: 1, maximum: 20, newTagsRequire: ["central=true", "reusable=true", "description"], newTagsStartAs: "pending" }
|
|
21368
21417
|
};
|
|
21369
21418
|
}
|
|
@@ -21377,16 +21426,18 @@ async function prepareLocalSourcebookWrite(input) {
|
|
|
21377
21426
|
...parsed.tags.filter((tag) => tag !== categoryTag).map((tag) => ({ tag }))
|
|
21378
21427
|
];
|
|
21379
21428
|
const tagResolutions = await resolveLocalSourcebookTags(candidates);
|
|
21429
|
+
const publicUrls = localSourcebookPublicUrls(parsed.category, parsed.state, slug2);
|
|
21380
21430
|
return {
|
|
21381
21431
|
ok: true,
|
|
21382
|
-
route: { category: parsed.category, state: parsed.state, slug: slug2,
|
|
21432
|
+
route: { category: parsed.category, state: parsed.state, slug: slug2, ...publicUrls },
|
|
21383
21433
|
proposedWrite: { ...parsed, slug: slug2, tags: tagResolutions.filter((item) => item.action === "reuse").map((item) => item.tag) },
|
|
21384
21434
|
contract: getLocalSourcebookContract(parsed.category),
|
|
21385
21435
|
tagResolutions,
|
|
21386
21436
|
instructions: [
|
|
21387
21437
|
"Resolve every review candidate before capture; do not silently create a near-duplicate tag.",
|
|
21388
21438
|
"Call validate-local-sourcebook-write with the proposed identity, resolved tags, and any required tag decisions.",
|
|
21389
|
-
"Call local-sourcebook-capture only after validation returns valid=true. Capture
|
|
21439
|
+
"Call local-sourcebook-capture only after validation returns valid=true. Capture queues paid acquisition and automatically publishes the resulting system-compiled evidence revision.",
|
|
21440
|
+
"Owners cannot supply public listing claims directly. Request local_sourcebook_refresh when source-backed public facts need to be reacquired."
|
|
21390
21441
|
]
|
|
21391
21442
|
};
|
|
21392
21443
|
}
|
|
@@ -21394,17 +21445,9 @@ async function validateLocalSourcebookWrite(input) {
|
|
|
21394
21445
|
const errors = [];
|
|
21395
21446
|
const warnings = [];
|
|
21396
21447
|
let normalizedIdentity2;
|
|
21397
|
-
|
|
21398
|
-
if (
|
|
21399
|
-
|
|
21400
|
-
const parsedListing = DirectoryListingDraftSchema.safeParse(input.listing);
|
|
21401
|
-
if (!parsedListing.success) errors.push(...parsedListing.error.issues.map((issue) => `listing.${issue.path.join(".")}: ${issue.message}`));
|
|
21402
|
-
else normalizedListing = parsedListing.data;
|
|
21403
|
-
} else {
|
|
21404
|
-
const parsedIdentity = DirectoryIdentitySchema.safeParse(input.identity);
|
|
21405
|
-
if (!parsedIdentity.success) errors.push(...parsedIdentity.error.issues.map((issue) => `identity.${issue.path.join(".")}: ${issue.message}`));
|
|
21406
|
-
else normalizedIdentity2 = parsedIdentity.data;
|
|
21407
|
-
}
|
|
21448
|
+
const parsedIdentity = DirectoryIdentitySchema.safeParse(input.identity);
|
|
21449
|
+
if (!parsedIdentity.success) errors.push(...parsedIdentity.error.issues.map((issue) => `identity.${issue.path.join(".")}: ${issue.message}`));
|
|
21450
|
+
else normalizedIdentity2 = parsedIdentity.data;
|
|
21408
21451
|
const candidates = input.tagCandidates ?? (normalizedIdentity2?.tags.map((tag) => ({ tag })) ?? []);
|
|
21409
21452
|
const resolutions = await resolveLocalSourcebookTags(candidates);
|
|
21410
21453
|
const decisions = new Map((input.tagDecisions ?? []).map((decision) => [normalizeTag(decision.tag), decision]));
|
|
@@ -21423,12 +21466,12 @@ async function validateLocalSourcebookWrite(input) {
|
|
|
21423
21466
|
}
|
|
21424
21467
|
if (resolution.action === "omit") warnings.push(`Tag \u201C${resolution.candidate}\u201D will be omitted: ${resolution.reason}`);
|
|
21425
21468
|
}
|
|
21426
|
-
if (
|
|
21469
|
+
if (normalizedIdentity2) {
|
|
21427
21470
|
const categoryTag = CATEGORY_TAG[normalizedIdentity2.category];
|
|
21428
21471
|
if (!normalizedTags.includes(categoryTag)) normalizedTags.unshift(categoryTag);
|
|
21429
21472
|
}
|
|
21430
21473
|
if (!normalizedTags.length) errors.push("At least one canonical directory tag is required.");
|
|
21431
|
-
return { ok: true, valid: errors.length === 0, errors, warnings, normalizedIdentity: normalizedIdentity2,
|
|
21474
|
+
return { ok: true, valid: errors.length === 0, errors, warnings, normalizedIdentity: normalizedIdentity2, normalizedTags: [...new Set(normalizedTags)], tagResolutions: resolutions };
|
|
21432
21475
|
}
|
|
21433
21476
|
async function persistLocalSourcebookTags(submissionId, userId, tags, decisions = []) {
|
|
21434
21477
|
await ensureLocalSourcebookTags();
|
|
@@ -21445,11 +21488,6 @@ async function persistLocalSourcebookTags(submissionId, userId, tags, decisions
|
|
|
21445
21488
|
await db.execute({ sql: `INSERT OR IGNORE INTO local_sourcebook_submission_tags (submission_id, tag) VALUES (?, ?)`, args: [submissionId, tag] });
|
|
21446
21489
|
}
|
|
21447
21490
|
}
|
|
21448
|
-
async function replaceLocalSourcebookTags(submissionId, userId, tags, decisions = []) {
|
|
21449
|
-
await ensureLocalSourcebookTags();
|
|
21450
|
-
await getDb().execute({ sql: `DELETE FROM local_sourcebook_submission_tags WHERE submission_id = ?`, args: [submissionId] });
|
|
21451
|
-
await persistLocalSourcebookTags(submissionId, userId, tags, decisions);
|
|
21452
|
-
}
|
|
21453
21491
|
|
|
21454
21492
|
// src/api/local-sourcebook-routes.ts
|
|
21455
21493
|
var slug = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 100);
|
|
@@ -21461,13 +21499,9 @@ var SubmitSchema = z26.object({
|
|
|
21461
21499
|
slug: z26.string().trim().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100).optional(),
|
|
21462
21500
|
idempotencyKey: z26.string().trim().min(8).max(200).optional()
|
|
21463
21501
|
}).strict();
|
|
21464
|
-
var DraftSchema = z26.object({ payload: z26.record(z26.string(), z26.unknown()) }).strict();
|
|
21465
21502
|
var PrepareWriteSchema = DirectoryIdentitySchema.extend({ tagCandidates: z26.array(DirectoryTagCandidateSchema).max(20).optional() });
|
|
21466
21503
|
var ValidateWriteSchema = z26.object({
|
|
21467
21504
|
identity: DirectoryIdentitySchema.optional(),
|
|
21468
|
-
listing: z26.record(z26.string(), z26.unknown()).optional(),
|
|
21469
|
-
submissionId: z26.string().trim().min(1).optional(),
|
|
21470
|
-
baseRevision: z26.number().int().min(1).optional(),
|
|
21471
21505
|
tagCandidates: z26.array(DirectoryTagCandidateSchema).max(20).optional(),
|
|
21472
21506
|
tagDecisions: z26.array(DirectoryTagDecisionSchema).max(20).optional()
|
|
21473
21507
|
}).strict();
|
|
@@ -21477,7 +21511,7 @@ function acquisitionPlan(websiteUrl) {
|
|
|
21477
21511
|
website: { startUrl: websiteUrl, mode: "broad_site_crawl", capture: ["homepage", "services", "products", "locations", "service_areas", "about", "team_staff", "contact", "policies"] },
|
|
21478
21512
|
media: { requirement: "genuine_business_images_only", sources: ["business_website", "business_team_pages", "verified_business_profiles"] },
|
|
21479
21513
|
reviews: { mode: "exhaust_accessible_continuation", requirement: "retain_each_review_and_source", disclosure: "report captured count, reported available count, and exact stopping reason" },
|
|
21480
|
-
publication: { automatic:
|
|
21514
|
+
publication: { automatic: true, evidenceCompiledOnly: true, adminReviewRequired: false }
|
|
21481
21515
|
};
|
|
21482
21516
|
}
|
|
21483
21517
|
var localSourcebookApp = new Hono17();
|
|
@@ -21507,18 +21541,12 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
|
|
|
21507
21541
|
const validation = await validateLocalSourcebookWrite(parsed.data);
|
|
21508
21542
|
if (!validation.valid) return c.json({ error: "invalid_directory_write", ...validation }, 400);
|
|
21509
21543
|
try {
|
|
21510
|
-
if (parsed.data.submissionId) {
|
|
21511
|
-
const submission2 = await getLocalSourcebookSubmission(parsed.data.submissionId, Number(user.id));
|
|
21512
|
-
if (!submission2) return c.json({ error: "not_found" }, 404);
|
|
21513
|
-
const updated = await reviseLocalSourcebookDraft(submission2.id, Number(user.id), validation.normalizedListing, parsed.data.baseRevision);
|
|
21514
|
-
await replaceLocalSourcebookTags(submission2.id, Number(user.id), validation.normalizedTags, parsed.data.tagDecisions);
|
|
21515
|
-
return c.json({ ok: true, valid: true, submission: updated, normalizedTags: validation.normalizedTags, captured: "draft_revision", acquisitionQueued: false });
|
|
21516
|
-
}
|
|
21517
21544
|
const identity = validation.normalizedIdentity;
|
|
21518
21545
|
if (!parsed.data.idempotencyKey && !identity.idempotencyKey) {
|
|
21519
21546
|
return c.json({ error: "idempotency_key_required", message: "A stable idempotencyKey is required for a new Local Sourcebook capture." }, 400);
|
|
21520
21547
|
}
|
|
21521
21548
|
const listingSlug = identity.slug ?? slug(identity.businessName);
|
|
21549
|
+
const publicUrls = localSourcebookPublicUrls(identity.category, identity.state, listingSlug);
|
|
21522
21550
|
const submission = await createLocalSourcebookSubmission({
|
|
21523
21551
|
ownerUserId: Number(user.id),
|
|
21524
21552
|
category: identity.category,
|
|
@@ -21535,7 +21563,8 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
|
|
|
21535
21563
|
name: identity.businessName,
|
|
21536
21564
|
websiteUrl: identity.websiteUrl,
|
|
21537
21565
|
tags: validation.normalizedTags,
|
|
21538
|
-
canonicalPaths: { profile:
|
|
21566
|
+
canonicalPaths: { profile: publicUrls.profilePath, reviews: publicUrls.reviewsPath },
|
|
21567
|
+
canonicalUrls: { profile: publicUrls.profileUrl, reviews: publicUrls.reviewsUrl },
|
|
21539
21568
|
verificationState: "submitted",
|
|
21540
21569
|
acquisitionPlan: acquisitionPlan(identity.websiteUrl),
|
|
21541
21570
|
evidence: [],
|
|
@@ -21545,7 +21574,7 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
|
|
|
21545
21574
|
});
|
|
21546
21575
|
await persistLocalSourcebookTags(submission.id, Number(user.id), validation.normalizedTags, parsed.data.tagDecisions);
|
|
21547
21576
|
const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
|
|
21548
|
-
return c.json({ ok: true, valid: true, submission, normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl) }, 202);
|
|
21577
|
+
return c.json({ ok: true, valid: true, submission, publicUrls: localSourcebookPublicationLinks(submission), normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl) }, 202);
|
|
21549
21578
|
} catch (error) {
|
|
21550
21579
|
const message = error instanceof Error ? error.message : String(error);
|
|
21551
21580
|
const conflict = message.includes("revision conflict") ? "revision_conflict" : message.includes("idempotency key conflicts") ? "idempotency_conflict" : null;
|
|
@@ -21559,6 +21588,7 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
|
|
|
21559
21588
|
if (!parsed.success) return c.json({ error: "invalid_submission", issues: parsed.error.issues }, 400);
|
|
21560
21589
|
const input = parsed.data;
|
|
21561
21590
|
const listingSlug = input.slug ?? slug(input.businessName);
|
|
21591
|
+
const publicUrls = localSourcebookPublicUrls(input.category, input.state, listingSlug);
|
|
21562
21592
|
try {
|
|
21563
21593
|
const submission = await createLocalSourcebookSubmission({
|
|
21564
21594
|
ownerUserId: Number(user.id),
|
|
@@ -21571,7 +21601,8 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
|
|
|
21571
21601
|
slug: listingSlug,
|
|
21572
21602
|
name: input.businessName,
|
|
21573
21603
|
websiteUrl: input.websiteUrl,
|
|
21574
|
-
canonicalPaths: { profile:
|
|
21604
|
+
canonicalPaths: { profile: publicUrls.profilePath, reviews: publicUrls.reviewsPath },
|
|
21605
|
+
canonicalUrls: { profile: publicUrls.profileUrl, reviews: publicUrls.reviewsUrl },
|
|
21575
21606
|
verificationState: "submitted",
|
|
21576
21607
|
acquisitionPlan: acquisitionPlan(input.websiteUrl),
|
|
21577
21608
|
evidence: [],
|
|
@@ -21580,7 +21611,7 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
|
|
|
21580
21611
|
}
|
|
21581
21612
|
});
|
|
21582
21613
|
const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
|
|
21583
|
-
return c.json({ ok: true, submission, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl) }, 202);
|
|
21614
|
+
return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl) }, 202);
|
|
21584
21615
|
} catch (error) {
|
|
21585
21616
|
return c.json({ error: "submission_conflict", message: error instanceof Error ? error.message : String(error) }, 409);
|
|
21586
21617
|
}
|
|
@@ -21589,26 +21620,27 @@ localSourcebookApp.get("/submissions/:id", createApiKeyAuth(), async (c) => {
|
|
|
21589
21620
|
const userId = Number(c.get("user").id);
|
|
21590
21621
|
const submission = await getLocalSourcebookSubmission(c.req.param("id"), userId);
|
|
21591
21622
|
if (!submission) return c.json({ error: "not_found" }, 404);
|
|
21592
|
-
return c.json({ ok: true, submission, draft: await getLocalSourcebookDraft(submission.id, userId) });
|
|
21593
|
-
});
|
|
21594
|
-
localSourcebookApp.on(["PATCH", "POST"], "/submissions/:id/draft", createApiKeyAuth(), async (c) => {
|
|
21595
|
-
const user = c.get("user");
|
|
21596
|
-
if (!user.subscription_tier) return c.json({ error: "subscription_required" }, 402);
|
|
21597
|
-
const parsed = DraftSchema.safeParse(await c.req.json().catch(() => null));
|
|
21598
|
-
if (!parsed.success) return c.json({ error: "invalid_draft", issues: parsed.error.issues }, 400);
|
|
21599
|
-
const submission = await reviseLocalSourcebookDraft(c.req.param("id"), Number(user.id), parsed.data.payload);
|
|
21600
|
-
if (!submission) return c.json({ error: "not_found" }, 404);
|
|
21601
|
-
return c.json({ ok: true, submission });
|
|
21623
|
+
return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), draft: await getLocalSourcebookDraft(submission.id, userId) });
|
|
21602
21624
|
});
|
|
21625
|
+
localSourcebookApp.on(["PATCH", "POST"], "/submissions/:id/draft", createApiKeyAuth(), (c) => c.json({
|
|
21626
|
+
error: "evidence_refresh_required",
|
|
21627
|
+
message: "Owner-authored listing payloads are not accepted. Request a refresh so MCP Scraper can reacquire and compile the public evidence."
|
|
21628
|
+
}, 409));
|
|
21603
21629
|
localSourcebookApp.post("/submissions/:id/refresh", createApiKeyAuth(), async (c) => {
|
|
21604
21630
|
const user = c.get("user");
|
|
21605
21631
|
if (!user.subscription_tier) return c.json({ error: "subscription_required" }, 402);
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21632
|
+
try {
|
|
21633
|
+
const submission = await queueLocalSourcebookRefresh(c.req.param("id"), Number(user.id));
|
|
21634
|
+
if (!submission) return c.json({ error: "not_found" }, 404);
|
|
21635
|
+
const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
|
|
21636
|
+
return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(submission.websiteUrl) }, 202);
|
|
21637
|
+
} catch (error) {
|
|
21638
|
+
return c.json({ error: "moderated_listing", message: error instanceof Error ? error.message : String(error) }, 409);
|
|
21639
|
+
}
|
|
21610
21640
|
});
|
|
21611
21641
|
var publicLocalSourcebookApp = new Hono17();
|
|
21642
|
+
publicLocalSourcebookApp.get("/", async (c) => c.json({ ok: true, listings: await listAllPublicLocalSourcebook() }));
|
|
21643
|
+
publicLocalSourcebookApp.get("/:category", async (c) => c.json({ ok: true, listings: await listAllPublicLocalSourcebook(c.req.param("category")) }));
|
|
21612
21644
|
publicLocalSourcebookApp.get("/:category/:state", async (c) => c.json({ ok: true, listings: await listPublicLocalSourcebook(c.req.param("category"), c.req.param("state").toLowerCase()) }));
|
|
21613
21645
|
publicLocalSourcebookApp.get("/:category/:state/:slug", async (c) => {
|
|
21614
21646
|
const listing = await getPublicLocalSourcebook(c.req.param("category"), c.req.param("state").toLowerCase(), c.req.param("slug"));
|
|
@@ -40986,4 +41018,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
40986
41018
|
export {
|
|
40987
41019
|
app
|
|
40988
41020
|
};
|
|
40989
|
-
//# sourceMappingURL=server-
|
|
41021
|
+
//# sourceMappingURL=server-JJLFY46K.js.map
|