## fix(security): bump tar to 7.5.16 across lockfiles (PAX file smuggling) Resolves [#1472 1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496 1498 1505](https://github.com/twentyhq/twenty/security/dependabot/1472 1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496 1498 1505). ### What `tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to intermediary GNU long-name/long-link headers, causing a tar-parser interpretation differential (file smuggling). Patched in `7.5.16`. ### Why these alerts The advisory is scanned across many independent Yarn projects, so it surfaced as one alert per lockfile: the root `yarn.lock` plus 13 `packages/twenty-apps/**` lockfiles (each pulls `tar` transitively). ### How - Refreshed `tar` to `7.5.16` in the root and all 13 app lockfiles — they hold `tar` via `^7.5.x` ranges that already permit it, so this is an in-range lockfile refresh (no override) via `yarn up -R tar`. - The root additionally had `tar@7.5.15` exact-pinned by `@mintlify/previewing`, which has **no upstream fix** (latest `4.0.1163` still pins `7.5.15`). Added a scoped resolution `@mintlify/previewing/tar -> ^7.5.16`, **extending the existing scoped tar resolutions** already used for `@electron/rebuild` and `@electron/node-gyp`. ### Not included `seed-dependencies/yarn.lock` (alert #1500) is intentionally excluded: that lockfile and its checksum constants are already modified by the open form-data PR, so its `tar` bump will follow separately to avoid a conflict. ### Verification - No `tar <= 7.5.15` remains across the root or any app lockfile. - `yarn install --immutable` passes.
People Data Labs enrichment app
Enriches Person and Company records with People Data Labs (PDL) data.
Status: data model + enrichment mapper. This package defines the fields, relation, views, role, and manifest, and implements the enrichment logic functions that call the PDL REST API and map the response onto the standard +
pdl*fields. The manual "Enrich" record-action workflows are currently created by hand — automatic post-install seeding is implemented but not wired up (see Seeded workflows).
Enrichment logic functions
enrich-companies / enrich-people (bulk workflow actions, for the manual record action) and
enrich-company / enrich-person (single-record functions exposed both as a workflow action
and as an AI tool) all delegate to a shared, trigger-agnostic core in src/logic-functions/handlers/:
-
The bulk workflow-action functions accept a list of records (
{ records, overrideExistingValues? }), call the PDL bulk Enrichment endpoints (/person/bulk,/company/enrich/bulk), and loop the single-record core over each, aggregating the outcome (total/matched/notFound/skipped/errored); a per-record failure is captured asERRORwithout aborting the batch (src/logic-functions/utils/run-batch-enrichment.ts). -
The single-record functions accept one record (
{ recordId, overrideExistingValues? }), call the PDL single-record Enrichment endpoints (/person/enrich,/company/enrich—src/logic-functions/utils/post-pdl-single-enrich.ts), and return a singleEnrichResult(src/logic-functions/utils/run-single-enrichment.ts). They declare both aworkflowActionTriggerSettingsand atoolTriggerSettings, so one function is usable as a workflow step and as an AI tool. -
Read the record, guard against re-enriching within a TTL (
pdlLastEnrichedAt), pick a match identifier (person:pdlId→ LinkedIn → email → name; company:pdlId→ domain → name), and call the PDL Person/Company Enrichment API (src/logic-functions/utils/). -
On a match: fill standard fields only when empty (never clobber user data), always (re)write
pdl*fields, and setpdlEnrichmentStatus = MATCHED,pdlLastEnrichedAt,pdlRawPayload(+pdlLikelihoodfor Person). PDL404→NOT_FOUND; other errors →ERROR. No identifier / fresh TTL → skipped with no writes. -
SELECT/MULTI_SELECT values are normalized and dropped if not in the field's option set (
src/logic-functions/utils/); the option sets are the samesrc/constants/*-options.tsthe field definitions use.
Run locally: yarn twenty dev:function:exec -n enrich-people -p '{"records":[{"id":"<id>"}]}' (bulk)
or yarn twenty dev:function:exec -n enrich-person -p '{"recordId":"<id>"}' (single record).
Billing
Each successful match is billed to the workspace in Twenty credits via
chargeCredits (twenty-sdk/billing), mirroring PDL's own model — PDL only consumes a
credit on a 200 match, so not_found, errors, and skipped records are free:
- Person match: 336,000 micro-credits ($0.336 — PDL list price $0.28 + 20% margin)
- Company match: 120,000 micro-credits ($0.12 — PDL list price $0.10 + 20% margin)
The charge is emitted once per PDL batch call (src/logic-functions/utils/enrich-chunk.ts)
with quantity = number of matches and resourceContext pdl/person / pdl/company,
at the moment PDL returns — a record whose subsequent write fails is still billed, since the
PDL cost was already incurred. Prices live in src/constants/*-match-cost-dollars.ts and
the margin in src/constants/billing-margin-multiplier.ts. Billing is non-fatal: a failed
charge never fails the enrichment.
Seeded workflows (post-install)
Not currently wired up.
post-install.function.tsis a no-op (return { seededWorkflows: [] }); the seeding implementation insrc/logic-functions/handlers/post-install.ts(postInstallCore) is not invoked. An app'sCoreApiClientonly exposes per-object CRUD over the workspace/graphqlschema, and the workflow-builder mutations needed to seed a workflow (createWorkflowVersionStep/activateWorkflowVersion) are core resolvers the app surface does not yet expose. Until the SDK exposes them, create the two "Enrich" workflows by hand.
When re-enabled, each workflow is a MANUAL / BULK_RECORDS trigger wired to a single
LOGIC_FUNCTION step whose records input is bound to the selected records
({{trigger.companies}} / {{trigger.people}}):
- Enrich companies — runs
enrich-companiesover the selected Companies. - Enrich people — runs
enrich-peopleover the selected People.
The intended seeding (postInstallCore) resolves each function's runtime id from its
universalIdentifier via the metadata API, publishes the version
(activateWorkflowVersion), and is idempotent (skips a workflow whose name already exists).
Deferred to a later PR: auto-enrichment triggers (on-create event + cron backfill).
Data-model decisions
Bundle scope
Only the core PDL company fields are defined. Premium / Comprehensive / specialized fields
(inferred_revenue, linkedin_follower_count, employee growth/churn/tenure, parent /
subsidiary, exec movement, top employers, funding_details, …) are out of scope for this app.
Enums → SELECT / MULTI_SELECT
Every PDL enum that has a canonical file is a SELECT, validated 0-missing/0-extra against PDL schema v34.1:
| Field | Type | Options |
|---|---|---|
pdlSeniority (job_title_levels, array) |
MULTI_SELECT | 10 |
pdlFundingStages (funding_stages, array) |
MULTI_SELECT | 29 |
pdlIndustry (industry) |
SELECT | 147 |
pdlJobTitleSubRole (job_title_sub_role) |
SELECT | 106 |
pdlJobTitleClass, pdlInferredSalary, pdlSex, pdlCompanyType, pdlSizeRange, pdlLatestFundingStage, pdlLocationContinent, pdlLocationMetro, pdlMicExchange |
SELECT | 5 / 11 / 2 / 6 / 8 / 29 / 7 / 384 / 70 |
- Option
values are normalized to GraphQL enum names (united states→UNITED_STATES): uppercase, accents stripped, non-alphanumeric →_, digit-leading prefixed. - Option
universalIdentifiers are unique per field (shared enums like industry, metro, and funding stage get a separate id-set per field). - The large option sets (
metro-options.ts,industry-options.ts, …) and the UUID registry (universal-identifiers.ts) are generated from the PDL taxonomy and checked in. When regenerating for a newer PDL schema, never change an existing option or field UUID — that orphans stored data; only append ids for new options.select-option-constants.spec.tsguards global UUID uniqueness, value normalization, and per-field id integrity. - Stays
TEXT(no canonical PDL enum file exists):pdlIndustryDetail(industry_v2),pdlJobOnetCode. PDLlocation_regionhas no dedicated field — it fills thestateslot of the personpdlLocationADDRESS composite.
Standard-field mapping
pdl* shadows are removed where an equivalent standard field exists; the mapper writes
the standard field instead:
| Object | Removed shadow → standard target |
|---|---|
| Person | pdlLinkedinUrl→linkedinLink, pdlJobTitle→jobTitle, pdlFullName→name, pdlWorkEmail/pdlPersonalEmails→emails, pdlMobilePhone/pdlPhoneNumbers→phones |
| Company | pdlLinkedinUrl→linkedinLink, pdlWebsite→domainName, pdlDisplayName→name |
Shadows are kept where no reliable standard field is available: pdlEmployeeCount,
pdlTwitterUrl.
Trade-off: PDL's work/personal-email and mobile/other-phone distinction is dropped (folded
into the standard bags).
Location → ADDRESS composite
- Company location → the standard
addresscomposite (street/city/state/postcode/country/geo). - Person has no standard address field → dedicated
pdlLocation(ADDRESS). pdlLocationMetro(both) andpdlLocationContinent(company) stay SELECT — ADDRESS has no slot. Trade-off: ADDRESScountryis free text, so the country SELECT was dropped.
Current company → standard company
PDL's detected current employer (job_company_*) is resolved to a Company record
(find-or-create, matched by pdlId → domain → LinkedIn → name; created with
name / domainName / linkedinLink + pdlId / pdlIndustry / pdlSizeRange when none
matches) and linked via the standard company relation, fill-only-if-empty — it never
overwrites a company the user already set, and the lookup is skipped entirely when the person
already has one (no orphan companies).
Company attributes live on the Company record, not denormalized on the Person. The earlier
pdlCurrentCompany / pdlCurrentEmployees relation and the six pdlJobCompany* scalar fields
were removed as duplicates of the standard company relation and the linked Company's own
fields.
Enrichment metadata
pdlId— PDL record id (re-enrich by id: more precise than by email).pdlLikelihood(Person, NUMBER) — PDL match confidence 1–10.pdlEnrichmentStatus(SELECT:MATCHED/NOT_FOUND/ERROR) — distinguishes "no match" from "never tried" (drives re-enrichment scheduling).pdlLastEnrichedAt(DATE_TIME),pdlRawPayload(RAW_JSON, full response).
Other
pdlTotalFundingisCURRENCY(mapper must convert the bare USD float → micros).- Views: a curated "People Data Labs" TABLE view per object.
- Role: read/update on Person & Company (object-level; tighten to field-scoped later).
What the mapper does
Orchestration (src/logic-functions/)
- Runs from the manual "Enrich" record action (
BULK_RECORDS) or the single-recordenrich-company/enrich-personfunctions (as a workflow step or an AI tool). - Calls the PDL Person / Company Enrichment API with
PDL_API_KEY, passing amin_likelihoodchosen by identifier strength (2 with a strong identifier, 6 for a weaker name-based match; overridable per call). - A match →
pdlEnrichmentStatus = MATCHED; PDL404/ no match →NOT_FOUND; other errors →ERROR. Errored and not-found records are also stamped withpdlLastEnrichedAtso the TTL guard backs off instead of re-submitting them on every run. - TTL guard: skips re-enrichment when
pdlLastEnrichedAtis within 7 days (bypass withforce), and prefers re-enriching bypdlId.
Field writing
- Standard fields are filled only when empty (never clobber user data): Person
name,emails,phones,linkedinLink,jobTitle; Companyname,domainName,linkedinLink,address. Allpdl*fields are (re)written on every match. - SELECT guard: a SELECT/MULTI_SELECT value is written only if its normalized form is in the
field's option set; otherwise it is skipped and preserved in
pdlRawPayload(handles PDL schema versions newer than the bundled one).job_title_levels→pdlSeniority,funding_stages→pdlFundingStages. - CURRENCY:
total_funding_raised(USD) →{ amountMicros: value × 1_000_000, currencyCode: 'USD' }. - ADDRESS: PDL
location.*is split into the composite — Company → standardaddress, Person →pdlLocation. - Current company:
job_company_*is resolved to a Company record (find-or-create, matched bypdlId→ domain → LinkedIn → name) and linked via the standardcompanyrelation (fill-only-if-empty); resolutions are cached within a batch run. - Dates: partial PDL dates (
YYYY,YYYY-MM) forjob_start_date,last_funding_date,birth_dateare expanded and range-validated. - Always sets
pdlId,pdlLastEnrichedAt,pdlRawPayload(+pdlLikelihoodfor Person).