From 4ff0543f8a5d5e40158f63a85c6b7334903e8b1b Mon Sep 17 00:00:00 2001 From: Zach Caceres Date: Wed, 8 Jul 2026 08:47:14 -0600 Subject: [PATCH 1/3] fix: align Domain API schemas with BuiltWith v23 docs Response schemas use z.strictObject(), so fields the live v23 API returns but the code omits cause domain()/domainLive() to throw on parse. Add the missing fields and correct the date-range separator. - Meta: add Umbrella (Global Router traffic rank) - Attributes: add EcommerceCategory, TTFB, SourceBytes - DomainParams: add includeTrust -> TRUST query param - FDRANGE/LDRANGE: pipe-joined YYYY-MM-DD|YYYY-MM-DD, not dash-joined - Update CLI/library docs and tests to match --- docs/cli.md | 5 +++-- docs/guide/library.md | 5 +++-- packages/builtwith-api/src/commands.ts | 15 +++++++++++++-- packages/builtwith-api/src/index.ts | 1 + packages/builtwith-api/src/schemas.ts | 13 +++++++++---- packages/builtwith-api/test/params.test.ts | 8 ++++++-- packages/builtwith-api/test/schemas.test.ts | 1 + 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index f7813f6..136f02c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -69,8 +69,9 @@ builtwith domain "example.com,other.com" | `--noMetaData` | Exclude metadata | | `--noAttributeData` | Exclude attribute data | | `--noPII` | Exclude personally identifiable information | -| `--firstDetectedRange` | Filter by first detected date range | -| `--lastDetectedRange` | Filter by last detected date range | +| `--includeTrust` | Include Trust API data (uses an additional API credit) | +| `--firstDetectedRange` | Filter by first detected date range (`YYYY-MM-DD` or `YYYY-MM-DD\|YYYY-MM-DD`) | +| `--lastDetectedRange` | Filter by last detected date range (`YYYY-MM-DD` or `YYYY-MM-DD\|YYYY-MM-DD`) | ### `domainLive` diff --git a/docs/guide/library.md b/docs/guide/library.md index 927b4b9..fe1e9ea 100644 --- a/docs/guide/library.md +++ b/docs/guide/library.md @@ -60,8 +60,9 @@ const filtered = await client.domain("example.com", { | `noMetaData` | `boolean` | Exclude company metadata | | `noAttributeData` | `boolean` | Exclude attribute data | | `noPII` | `boolean` | Exclude personally identifiable information | -| `firstDetectedRange` | `string` | Filter by first detected date range | -| `lastDetectedRange` | `string` | Filter by last detected date range | +| `includeTrust` | `boolean` | Include Trust API data (uses an additional API credit) | +| `firstDetectedRange` | `string` | Filter by first detected date range (`YYYY-MM-DD` or `YYYY-MM-DD\|YYYY-MM-DD`) | +| `lastDetectedRange` | `string` | Filter by last detected date range (`YYYY-MM-DD` or `YYYY-MM-DD\|YYYY-MM-DD`) | ### `domainLive(lookup)` diff --git a/packages/builtwith-api/src/commands.ts b/packages/builtwith-api/src/commands.ts index ecf3892..9cce7df 100644 --- a/packages/builtwith-api/src/commands.ts +++ b/packages/builtwith-api/src/commands.ts @@ -54,13 +54,24 @@ export const commands: CommandDefinition[] = [ { name: "noMetaData", description: "Exclude metadata", type: "boolean", required: false }, { name: "noAttributeData", description: "Exclude attribute data", type: "boolean", required: false }, { name: "noPII", description: "Exclude personally identifiable information", type: "boolean", required: false }, + { + name: "includeTrust", + description: "Include Trust API data (uses an additional API credit)", + type: "boolean", + required: false, + }, { name: "firstDetectedRange", - description: "Filter by first detected date range", + description: "Filter by first detected date range (YYYY-MM-DD|YYYY-MM-DD)", + type: "string", + required: false, + }, + { + name: "lastDetectedRange", + description: "Filter by last detected date range (YYYY-MM-DD|YYYY-MM-DD)", type: "string", required: false, }, - { name: "lastDetectedRange", description: "Filter by last detected date range", type: "string", required: false }, ], execute: (client, args) => { const lookup = splitLookup(args.lookup); diff --git a/packages/builtwith-api/src/index.ts b/packages/builtwith-api/src/index.ts index 274a1bb..d94b95d 100644 --- a/packages/builtwith-api/src/index.ts +++ b/packages/builtwith-api/src/index.ts @@ -41,6 +41,7 @@ const DOMAIN_BOOLEANS: BooleanMapping = { noMetaData: "NOMETA", noAttributeData: "NOATTR", noPII: "NOPII", + includeTrust: "TRUST", }; /** diff --git a/packages/builtwith-api/src/schemas.ts b/packages/builtwith-api/src/schemas.ts index ede4f21..2e51fd2 100644 --- a/packages/builtwith-api/src/schemas.ts +++ b/packages/builtwith-api/src/schemas.ts @@ -26,8 +26,8 @@ export const ClientOptionsSchema = z.strictObject({ */ export type ClientOptions = z.infer; -/** Matches YYYY-MM-DD or YYYY-MM-DD-YYYY-MM-DD date range format. */ -const dateRangePattern = /^\d{4}-\d{2}-\d{2}(-\d{4}-\d{2}-\d{2})?$/; +/** Matches YYYY-MM-DD or a pipe-joined YYYY-MM-DD|YYYY-MM-DD range (v23 FDRANGE/LDRANGE format). */ +const dateRangePattern = /^\d{4}-\d{2}-\d{2}(\|\d{4}-\d{2}-\d{2})?$/; /** Validation schema for {@link DomainParams}. */ export const DomainParamsSchema = z.strictObject({ @@ -37,8 +37,9 @@ export const DomainParamsSchema = z.strictObject({ noMetaData: z.boolean().optional(), noAttributeData: z.boolean().optional(), noPII: z.boolean().optional(), - firstDetectedRange: z.string().regex(dateRangePattern, "Expected YYYY-MM-DD or YYYY-MM-DD-YYYY-MM-DD").optional(), - lastDetectedRange: z.string().regex(dateRangePattern, "Expected YYYY-MM-DD or YYYY-MM-DD-YYYY-MM-DD").optional(), + includeTrust: z.boolean().optional(), + firstDetectedRange: z.string().regex(dateRangePattern, "Expected YYYY-MM-DD or YYYY-MM-DD|YYYY-MM-DD").optional(), + lastDetectedRange: z.string().regex(dateRangePattern, "Expected YYYY-MM-DD or YYYY-MM-DD|YYYY-MM-DD").optional(), }); /** * Optional parameters for the Domain API endpoint. @@ -171,6 +172,7 @@ const PathSchema = z.strictObject({ const MetaSchema = z.strictObject({ Majestic: z.number(), + Umbrella: z.number(), Vertical: z.string(), Social: z.array(z.string()), CompanyName: z.string(), @@ -213,6 +215,9 @@ const AttributesSchema = z.strictObject({ BWRank: z.number().optional(), Tranco: z.number().optional(), BWS: z.number().optional(), + EcommerceCategory: z.number().optional(), + TTFB: z.number().optional(), + SourceBytes: z.number().optional(), AIMaturity: z.number().optional(), AIOpenness: z.number().optional(), AIReadiness: z.number().optional(), diff --git a/packages/builtwith-api/test/params.test.ts b/packages/builtwith-api/test/params.test.ts index 7fb3d22..759958f 100644 --- a/packages/builtwith-api/test/params.test.ts +++ b/packages/builtwith-api/test/params.test.ts @@ -114,8 +114,12 @@ describe("date range validation", () => { expect(() => DomainParamsSchema.parse({ firstDetectedRange: "2024-01-15" })).not.toThrow(); }); - it("accepts YYYY-MM-DD-YYYY-MM-DD range format", () => { - expect(() => DomainParamsSchema.parse({ firstDetectedRange: "2020-01-01-2024-12-31" })).not.toThrow(); + it("accepts pipe-joined YYYY-MM-DD|YYYY-MM-DD range format", () => { + expect(() => DomainParamsSchema.parse({ firstDetectedRange: "2020-01-01|2024-12-31" })).not.toThrow(); + }); + + it("rejects the legacy dash-joined range format", () => { + expect(() => DomainParamsSchema.parse({ firstDetectedRange: "2020-01-01-2024-12-31" })).toThrow(); }); it("accepts lastDetectedRange with valid format", () => { diff --git a/packages/builtwith-api/test/schemas.test.ts b/packages/builtwith-api/test/schemas.test.ts index 1dcf803..06bad5d 100644 --- a/packages/builtwith-api/test/schemas.test.ts +++ b/packages/builtwith-api/test/schemas.test.ts @@ -86,6 +86,7 @@ describe("DomainResponseSchema", () => { }, Meta: { Majestic: 12345, + Umbrella: 6423, Vertical: "Technology", Social: ["https://twitter.com/example"], CompanyName: "Example Inc", From 4ad30f68a7ad22b717879ae2001f39a8b93691cf Mon Sep 17 00:00:00 2001 From: Zach Caceres Date: Wed, 8 Jul 2026 09:00:14 -0600 Subject: [PATCH 2/3] fix: mark intermittently-absent Domain fields optional Validated DomainResponseSchema against live v23 payloads (cnn.com, allbirds.com, example.com, neverssl.com, stripe.com). BuiltWith omits any field it has no data for, so several fields the docs list as present are domain-dependent: - Meta.Umbrella: absent on all sampled domains -> optional (docs overstate it) - Attributes.Employees: absent on cnn.com (pre-existing latent bug) -> optional - Attributes.Followers: absent on neverssl.com -> optional strictObject still rejects genuinely new/unknown keys, so drift detection is unaffected. --- packages/builtwith-api/src/schemas.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/builtwith-api/src/schemas.ts b/packages/builtwith-api/src/schemas.ts index 2e51fd2..435f49e 100644 --- a/packages/builtwith-api/src/schemas.ts +++ b/packages/builtwith-api/src/schemas.ts @@ -172,7 +172,7 @@ const PathSchema = z.strictObject({ const MetaSchema = z.strictObject({ Majestic: z.number(), - Umbrella: z.number(), + Umbrella: z.number().optional(), Vertical: z.string(), Social: z.array(z.string()), CompanyName: z.string(), @@ -207,8 +207,8 @@ const AttributesSchema = z.strictObject({ CDimensions: z.number(), CGoals: z.number(), CMetrics: z.number(), - Followers: z.number(), - Employees: z.number(), + Followers: z.number().optional(), + Employees: z.number().optional(), ProductCount: z.number().optional(), Revenue: z.number().optional(), PageRank: z.number().optional(), From d23e73d1bcbc2b114b9ded7cafb48cdfbaf3b152 Mon Sep 17 00:00:00 2001 From: Zach Caceres Date: Wed, 8 Jul 2026 09:09:26 -0600 Subject: [PATCH 3/3] fix: mark Meta.Social optional 39-domain live sweep (parked, foreign, ecommerce, SaaS, media, blog, gov/edu, nonprofit): Meta.Social absent on 8 domains (example.org/net, iana.org, rakuten.co.jp, naver.com, wikipedia.org, reddit.com, overreacted.io). Sweep surfaced zero unknown keys, confirming the strictObject field set is complete for v23. --- packages/builtwith-api/src/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builtwith-api/src/schemas.ts b/packages/builtwith-api/src/schemas.ts index 435f49e..48f91b2 100644 --- a/packages/builtwith-api/src/schemas.ts +++ b/packages/builtwith-api/src/schemas.ts @@ -174,7 +174,7 @@ const MetaSchema = z.strictObject({ Majestic: z.number(), Umbrella: z.number().optional(), Vertical: z.string(), - Social: z.array(z.string()), + Social: z.array(z.string()).optional(), CompanyName: z.string(), Telephones: z.array(z.string()), Emails: z.array(z.string()),