Skip to content

Commit 366282f

Browse files
authored
Merge pull request #475 from shamanic-technologies/main
chore: sync hotfix v0.39.90 to staging
2 parents 1329194 + 1bf5441 commit 366282f

16 files changed

Lines changed: 1062 additions & 34 deletions

‎.vt-a.log‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
RUN v4.1.12 /Users/kevinlourd/conductor/worktrees/factory-p3-next-apollo-service
3+
4+
5+
Test Files 80 passed | 3 skipped (83)
6+
Tests 852 passed | 32 skipped (884)
7+
Start at 21:47:33
8+
Duration 34.60s (transform 1.36s, setup 450ms, import 9.29s, tests 18.05s, environment 4ms)
9+

‎.vt-s.log‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
RUN v4.1.12 /Users/kevinlourd/conductor/worktrees/factory-p3-next-apollo-service
3+
4+
5+
Test Files 1 passed (1)
6+
Tests 4 passed (4)
7+
Start at 21:45:58
8+
Duration 357ms (transform 96ms, setup 23ms, import 22ms, tests 230ms, environment 0ms)
9+

‎CLAUDE.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,9 @@ pre-serve verification moved here from human-service. `/enrich`, `/match` and
662662
receive the verdict and its cost. Do not make it opt-in per caller; the
663663
owner's rule is that no revealed email leaves unverified.
664664

665+
### The waterfall DECISION can run as a stored script (FACTORY-P3 next outcomes, email_found)
666+
`src/lib/email-verify-script.ts`. No workflow step calls the waterfall, so it is a SERVICE switch held by workflow-service (`GET /internal/scripts/switches/service?outcome=email_found&callerService=apollo`, read at most once a minute; unreadable = last state, never read = our code). After the 30-day reuse (a held FINAL verdict asks nothing; a held step-1 catch_all/unknown is handed to the decision as `held`), the decision runs either HERE (`stepOne` + `secondOpinion` on its own `email-verification` run, child of the caller's run: Monitoring's service path) or, switched and with a caller run + user, as the stored script (`POST /internal/scripts/run`, `recordedBy: "apollo"`, parent = the caller's run). The script reaches us only through two DOORS that serve a verification IN FLIGHT here (`x-caller-run-id` + address, `registerInFlight`): `POST /internal/email-verifier-calls {verifier, email}` (our exact verifier call: hold, run under the SCRIPT's run, bronze row naming the CALLER's run (`VerificationContext.callRunParentId` moves only the run parent), email_checked, Apify-out timer, BounceBan resume/poll) and `GET /internal/email-verifier-calls/apify`. A refusal a door meets (billing, key, BounceBan pending, every verifier failed) is kept on the in-flight entry and re-thrown to the caller as the SAME `EmailVerificationError` (routes answer as before). EVERY decision, both paths, is recorded with its calls in the script's words (`email_verification_decisions`, migration 0048, fail-soft), served `GET /internal/email-verification-decisions/recorded?after=<seq>` (service key, 60 s settle) to workflow-service's case recorder. Guards: `email-verify-script.test.ts`, decision-run assertions in `email-verification*.test.ts` / `apify-credit-alert.test.ts`.
667+
665668
## Reveal domain gate — no Apollo credit on a domain that cannot verify `valid`
666669

667670
`POST /enrich` (Apollo path, cache miss) judges the employer's MAIL DOMAIN
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- FACTORY-P3 next outcomes (email_found): every verification DECISION (which verifier was asked, in which order, what
2+
-- each answered, what was decided) on either path: our own waterfall (`service`) or the stored script while switched
3+
-- (`script`). Bronze, append-only. Read only by GET /internal/email-verification-decisions/recorded (workflow-service's
4+
-- case recorder). Personal data.
5+
CREATE TABLE IF NOT EXISTS "email_verification_decisions" (
6+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
7+
"seq" bigserial NOT NULL,
8+
"email" text NOT NULL,
9+
"org_id" uuid NOT NULL,
10+
"caller_run_id" text,
11+
"run_id" text,
12+
"source" text,
13+
"path" text NOT NULL,
14+
"held" jsonb,
15+
"calls" jsonb NOT NULL,
16+
"result" jsonb,
17+
"error" text,
18+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
19+
CONSTRAINT "email_verification_decisions_path_check" CHECK ("path" IN ('service', 'script'))
20+
);
21+
--> statement-breakpoint
22+
CREATE UNIQUE INDEX IF NOT EXISTS "idx_email_verification_decisions_seq" ON "email_verification_decisions" ("seq");
23+
--> statement-breakpoint
24+
CREATE INDEX IF NOT EXISTS "idx_email_verification_decisions_created" ON "email_verification_decisions" ("created_at");

‎drizzle/meta/_journal.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,13 @@
337337
"when": 1781481600000,
338338
"tag": "0047_box_load_apollo_waste",
339339
"breakpoints": true
340+
},
341+
{
342+
"idx": 48,
343+
"version": "7",
344+
"when": 1781568000000,
345+
"tag": "0048_email_verification_decisions",
346+
"breakpoints": true
340347
}
341348
]
342349
}

‎openapi.json‎

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9502,6 +9502,264 @@
95029502
}
95039503
}
95049504
},
9505+
"/internal/email-verifier-calls": {
9506+
"post": {
9507+
"summary": "One verifier call for the stored verification script (its door; service key)",
9508+
"description": "FACTORY-P3 next outcomes (email_found). Serves ONLY a verification in flight in this service (`x-caller-run-id` = the run that asked this service, + the address): our own verifier call (ContactOut, BounceVerify, TryKitt, MillionVerifier, or BounceBan with its resume and polling) on the script's run (`x-run-id`), with its billing hold, run, bronze row (naming the caller's run) and email_checked declaration. 409 when nothing is in flight; 502 = refused (billing, key), the refusal reaching the caller as our own error.",
9509+
"parameters": [
9510+
{
9511+
"schema": {
9512+
"type": "string"
9513+
},
9514+
"required": true,
9515+
"name": "x-api-key",
9516+
"in": "header"
9517+
},
9518+
{
9519+
"schema": {
9520+
"type": "string"
9521+
},
9522+
"required": true,
9523+
"name": "x-org-id",
9524+
"in": "header"
9525+
},
9526+
{
9527+
"schema": {
9528+
"type": "string"
9529+
},
9530+
"required": true,
9531+
"name": "x-user-id",
9532+
"in": "header"
9533+
},
9534+
{
9535+
"schema": {
9536+
"type": "string"
9537+
},
9538+
"required": true,
9539+
"name": "x-run-id",
9540+
"in": "header"
9541+
},
9542+
{
9543+
"schema": {
9544+
"type": "string"
9545+
},
9546+
"required": true,
9547+
"name": "x-caller-run-id",
9548+
"in": "header"
9549+
}
9550+
],
9551+
"requestBody": {
9552+
"required": true,
9553+
"content": {
9554+
"application/json": {
9555+
"schema": {
9556+
"type": "object",
9557+
"properties": {
9558+
"verifier": {
9559+
"type": "string",
9560+
"enum": [
9561+
"contactout",
9562+
"bounceverify",
9563+
"trykitt",
9564+
"millionverifier",
9565+
"bounceban"
9566+
]
9567+
},
9568+
"email": {
9569+
"type": "string"
9570+
}
9571+
},
9572+
"required": [
9573+
"verifier",
9574+
"email"
9575+
]
9576+
}
9577+
}
9578+
}
9579+
},
9580+
"responses": {
9581+
"200": {
9582+
"description": "The call's bronze row (error = the verifier did not answer)",
9583+
"content": {
9584+
"application/json": {
9585+
"schema": {
9586+
"type": "object",
9587+
"properties": {
9588+
"verificationId": {
9589+
"type": "string"
9590+
},
9591+
"verifier": {
9592+
"type": "string"
9593+
},
9594+
"verdict": {
9595+
"type": "string",
9596+
"nullable": true
9597+
},
9598+
"error": {
9599+
"type": "string",
9600+
"nullable": true
9601+
}
9602+
},
9603+
"required": [
9604+
"verificationId",
9605+
"verifier",
9606+
"verdict",
9607+
"error"
9608+
]
9609+
}
9610+
}
9611+
}
9612+
},
9613+
"400": {
9614+
"description": "Missing header or field"
9615+
},
9616+
"409": {
9617+
"description": "No verification of this address in flight for that run"
9618+
},
9619+
"502": {
9620+
"description": "Refused (billing, key)"
9621+
}
9622+
}
9623+
}
9624+
},
9625+
"/internal/email-verifier-calls/apify": {
9626+
"get": {
9627+
"summary": "Whether BounceVerify is asked now (Apify not out of usage), for the stored verification script",
9628+
"parameters": [
9629+
{
9630+
"schema": {
9631+
"type": "string"
9632+
},
9633+
"required": true,
9634+
"name": "email",
9635+
"in": "query"
9636+
},
9637+
{
9638+
"schema": {
9639+
"type": "string"
9640+
},
9641+
"required": true,
9642+
"name": "x-api-key",
9643+
"in": "header"
9644+
},
9645+
{
9646+
"schema": {
9647+
"type": "string"
9648+
},
9649+
"required": true,
9650+
"name": "x-org-id",
9651+
"in": "header"
9652+
},
9653+
{
9654+
"schema": {
9655+
"type": "string"
9656+
},
9657+
"required": true,
9658+
"name": "x-user-id",
9659+
"in": "header"
9660+
},
9661+
{
9662+
"schema": {
9663+
"type": "string"
9664+
},
9665+
"required": true,
9666+
"name": "x-caller-run-id",
9667+
"in": "header"
9668+
}
9669+
],
9670+
"responses": {
9671+
"200": {
9672+
"description": "Availability",
9673+
"content": {
9674+
"application/json": {
9675+
"schema": {
9676+
"type": "object",
9677+
"properties": {
9678+
"available": {
9679+
"type": "boolean"
9680+
}
9681+
},
9682+
"required": [
9683+
"available"
9684+
]
9685+
}
9686+
}
9687+
}
9688+
},
9689+
"409": {
9690+
"description": "No verification in flight"
9691+
}
9692+
}
9693+
}
9694+
},
9695+
"/internal/email-verification-decisions/recorded": {
9696+
"get": {
9697+
"summary": "Every verification decision with each vendor call and what was decided (service key; personal data)",
9698+
"description": "FACTORY-P3 next outcomes (email_found). Oldest first by `seq`, a row served 60 s after it was written. `path` = `service` (our waterfall on its own `email-verification` run) or `script` (the stored script while switched). `calls` = each vendor call in the stored script's words (`apollo.email.verify` {verifier, email} -> the bronze row; `apollo.apify.available`), `held` = the step-1 verdict reused (then only BounceBan runs), `result` = what was decided. Read by workflow-service's case recorder.",
9699+
"parameters": [
9700+
{
9701+
"schema": {
9702+
"type": "string"
9703+
},
9704+
"required": false,
9705+
"name": "after",
9706+
"in": "query"
9707+
},
9708+
{
9709+
"schema": {
9710+
"type": "string",
9711+
"description": "1..1000, default 200"
9712+
},
9713+
"required": false,
9714+
"description": "1..1000, default 200",
9715+
"name": "limit",
9716+
"in": "query"
9717+
},
9718+
{
9719+
"schema": {
9720+
"type": "string"
9721+
},
9722+
"required": true,
9723+
"name": "x-api-key",
9724+
"in": "header"
9725+
}
9726+
],
9727+
"responses": {
9728+
"200": {
9729+
"description": "One page",
9730+
"content": {
9731+
"application/json": {
9732+
"schema": {
9733+
"type": "object",
9734+
"properties": {
9735+
"decisions": {
9736+
"type": "array",
9737+
"items": {
9738+
"type": "object",
9739+
"additionalProperties": {
9740+
"nullable": true
9741+
}
9742+
}
9743+
},
9744+
"nextAfter": {
9745+
"type": "integer"
9746+
},
9747+
"hasMore": {
9748+
"type": "boolean"
9749+
}
9750+
},
9751+
"required": [
9752+
"decisions",
9753+
"nextAfter",
9754+
"hasMore"
9755+
]
9756+
}
9757+
}
9758+
}
9759+
}
9760+
}
9761+
}
9762+
},
95059763
"/internal/email-verifications/history": {
95069764
"get": {
95079765
"summary": "Every email verification ever made, as a resumable feed",

‎src/db/schema.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,30 @@ export const emailVerifications = pgTable(
466466
]
467467
);
468468

469+
// FACTORY-P3 next outcomes (email_found, migration 0048): every verification DECISION with each vendor call in the
470+
// stored script's words (`calls`: name, args, answer) and what was decided, on either path (`service` = our waterfall
471+
// on its own `email-verification` run, `script` = the stored script while switched). Read only by
472+
// GET /internal/email-verification-decisions/recorded. Personal data: never logged.
473+
export const emailVerificationDecisions = pgTable(
474+
"email_verification_decisions",
475+
{
476+
id: uuid("id").primaryKey().defaultRandom(),
477+
seq: bigserial("seq", { mode: "number" }).notNull(),
478+
email: text("email").notNull(),
479+
orgId: uuid("org_id").notNull(),
480+
callerRunId: text("caller_run_id"),
481+
runId: text("run_id"),
482+
source: text("source"),
483+
path: text("path").notNull(),
484+
held: jsonb("held"),
485+
calls: jsonb("calls").notNull(),
486+
result: jsonb("result"),
487+
error: text("error"),
488+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
489+
},
490+
(table) => [uniqueIndex("idx_email_verification_decisions_seq").on(table.seq), index("idx_email_verification_decisions_created").on(table.createdAt)]
491+
);
492+
469493
// ─── QuickEnrich (free candidate source) ────────────────────────────────────
470494
// Bronze: every QuickEnrich search call through treg, verbatim.
471495
export const quickenrichSearches = pgTable(

‎src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import transferBrandRoutes from "./routes/transfer-brand.js";
1717
import phoneRevealRoutes from "./routes/phone-reveal.js";
1818
import emailFinderRoutes from "./routes/email-finder.js";
1919
import emailVerificationRoutes from "./routes/email-verifications.js";
20+
import emailVerifierCallRoutes from "./routes/email-verifier-calls.js";
2021
import verificationHistoryRoutes from "./routes/verification-history.js";
2122
import acquisitionCostRoutes from "./routes/acquisition-costs.js";
2223
import webhookRoutes from "./routes/webhook.js";
@@ -71,6 +72,7 @@ app.use(matchRoutes);
7172
app.use(phoneRevealRoutes);
7273
app.use(emailFinderRoutes);
7374
app.use(emailVerificationRoutes);
75+
app.use(emailVerifierCallRoutes); // FACTORY-P3 next outcomes: the stored verification script's doors + recorded decisions
7476
app.use(verificationHistoryRoutes);
7577
app.use(acquisitionCostRoutes);
7678
app.use(transferBrandRoutes);

0 commit comments

Comments
 (0)