Skip to content

feat: add public labels v3 adapter - #87

Draft
Seranged wants to merge 2 commits into
mainfrom
feat/public-labels-v3-sdk
Draft

feat: add public labels v3 adapter#87
Seranged wants to merge 2 commits into
mainfrom
feat/public-labels-v3-sdk

Conversation

@Seranged

@Seranged Seranged commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add an opt-in SDK adapter and focused public-labels package entrypoint for the immutable Public Labels V3 dataset.
  • Preserve the existing file-based EulerLabelsService default while Lite adopts and validates the new source in feat: integrate public labels euler-lite#779.

Changes

  • Resolve latest once and pin the complete snapshot to its concrete publication version.
  • Follow meta.total pagination with 100-record pages for vaults, products, entities, entity addresses, and geo policies.
  • Fetch managing and co-brand entity profiles with bounded concurrency and keep co-branding display-only.
  • Normalize products, vault overrides, entities, campaigns, Earn labels, deprecation metadata, and tags into EulerLabelsData-compatible structures.
  • Keep assessment-only inventory rows and neutral escrows out of label verification state.
  • Expose raw geo policies as informational data without deriving eligibility or visibility.
  • Export the adapter, normalizer, DTOs, label types, and branding helpers from @eulerxyz/euler-v2-sdk/public-labels so server consumers avoid loading the full SDK barrel.
  • Add deterministic KPK and Securitize fixtures, public helpers, and current-state integration docs.

Test plan

  • pnpm --filter @eulerxyz/euler-v2-sdk lint
  • pnpm --filter @eulerxyz/euler-v2-sdk typecheck
  • pnpm --filter @eulerxyz/euler-v2-sdk test (35 files, 519 tests)
  • pnpm --filter @eulerxyz/euler-v2-sdk build
  • Pack/install the pushed SDK branch into Lite and verify the root and public-labels runtime exports
  • Run the full paired Lite build and test suite (172 files, 1602 tests)
  • Smoke the built adapter against production and verify version v20260804151305236, paginated totals, KPK ownership, Securitize co-branding, and both hosted logos

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe024283-d475-47e1-bdea-dd118249afc2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Add a focused package entrypoint for the public labels adapter, normalizer, types, and branding helpers so consumers can avoid loading the full SDK barrel.

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Draft review on exact head 18145a8738d852a93fe09cbbd57a108d4d65bd69. I found five changes needed across verification-state semantics, scoped entity attribution, the focused declaration surface, untrusted product-key handling, and post-failure concurrency. Local validation otherwise passed: focused 8/8, full SDK 519/519, typecheck, build, and lint (existing warnings only); packed runtime imports and the live immutable KPK/Securitize fixture also loaded successfully. A strict packed subpath declaration probe failed as described inline. The review event is COMMENT because the PR is still a draft.

vault.isDeprecated ||
vault.deprecationReason ||
vault.tags.length ||
vault.campaigns?.length,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · blocking: campaigns are rewards metadata, but including vault.campaigns?.length in this verification-content predicate promotes points-only inventory rows into trust state. The live pinned snapshot has EVKs 0x35d4f830543700B7280084280ae3236f178E88e3 and 0xbC35161043EE2D74816d421EfD6a45fDa73B050A with campaigns and no product/entity/name/deprecation/tag content. Both exist in euler-labels/1/points.json and not 1/products.json; this normalizer nevertheless creates __vault_* products and adds them to verifiedVaultAddresses. Keep campaign extraction over all chain inventory rows, but exclude campaigns from the predicate that admits rows into products/verification/Earn membership. Add a campaign-only EVK regression proving points remain present while no product or verified address is created.

`Public Labels entity profile mismatch for ${entityId}`,
);
}
return { profile, addresses };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · blocking trust boundary: validate that every address row returned from /entities/${entityId}/addresses has row.entityId === entityId before flattening it. Profiles get this scoped-identity check above, but address rows do not; normalization later groups them by the row's claimed entityId. I reproduced /entities/brand/addresses returning { entityId: "owner", address: attacker }, after which entities.owner.addresses contains the attacker address. The linked Lite consumer builds hasEntityAddress directly from these sets for governor/owner verification. Reject cross-keyed rows here and add a co-brand-to-owner regression. Current production rows are consistent; the missing guard is what permits a malformed or cache-misrouted response to cross the identity boundary.

export {
getEulerLabelProductBrandEntities,
getEulerLabelProductBrandEntityKeys,
} from "./utils/eulerLabels.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · package compatibility: this makes the focused subpath's declaration graph broad again. utils/eulerLabels.d.ts imports EulerEarn, which pulls in unrelated vault/service declarations; in a clean packed consumer importing only @eulerxyz/euler-v2-sdk/public-labels with skipLibCheck: false, tsc reaches eVaultLensTypes.d.ts and fails on its package-internal src/utils/oracle.js import (plus unrelated dependency declarations). Runtime resolution and skipLibCheck: true pass, but the advertised focused type surface is not isolated. Move these two label-only helpers into a narrow module that imports only EulerLabelProduct/EulerLabelEntity, re-export it from both entrypoints, and add a strict packed-consumer declaration smoke test.


const products: Record<string, EulerLabelProduct> = {};
for (const product of productRows) {
products[product.id] = buildProduct(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · untrusted key handling: product.id comes from the remote dataset and is assigned into a normal {} record. With a product ID of __proto__, this assignment mutates the record's prototype instead of creating an enumerable product; the product disappears from Object.keys/Object.values, while its vault is still added to verifiedVaultAddresses. Use a null-prototype record/Map (and preferably validate IDs before object-key use), then add an exact __proto__ regression so malformed publication data cannot create split-brain verification state.

let nextIndex = 0;

const worker = async () => {
while (nextIndex < values.length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · failure liveness: once one mapper rejects, Promise.all rejects to the caller but the other workers keep claiming fresh indices. In a 20-entity fixture with the first profile request failing, there were 16 detail requests at caller rejection and all 40 profile/address requests were eventually issued. That spends API quota/bandwidth—and can keep sending an API key—after the operation is already known to have failed. Record a shared failure/stop flag so workers do not claim new indices after the first error (in-flight calls may finish), and add a deferred-request regression that checks the count does not continue to the full batch.

@LeonardEulerXYZ

LeonardEulerXYZ commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Overview

This PR adds an opt-in Public Labels V3 transport and normalizer, keeps the file-backed labels service as the default, and exposes a focused @eulerxyz/euler-v2-sdk/public-labels entrypoint for the paired Lite integration.

Review scope

  • immutable-version resolution and paginated fetches
  • entity/profile/address attribution and curator vs co-brand separation
  • vault/product/Earn/campaign normalization and verification-state boundaries
  • raw geo-policy handling
  • package root and public-labels runtime/type exports
  • linked euler-lite#779 producer/consumer composition
  • focused/full SDK validation and live production fixtures

Reviewed head: 18145a8738d852a93fe09cbbd57a108d4d65bd69
Review effort: High — labels feed user-facing verification and risk-manager identity.

Verdict

Changes requested on this draft:

  1. Campaign metadata currently promotes rewards-only inventory rows into vault verification state. The live immutable snapshot has two Ethereum EVKs (0x35d4…E88e3, 0xbC351…B050A) that exist in points.json but not products.json; normalization nevertheless creates standalone products and adds both to verifiedVaultAddresses because campaigns count as label content.
  2. Entity-address rows are flattened without proving that each row belongs to the scoped /entities/{id}/addresses endpoint. A cross-keyed co-brand response can therefore insert an arbitrary address into the managing entity’s address set, which the linked Lite consumer uses for governor/owner verification.
  3. The focused public-labels declaration surface re-exports helpers from the broad utils/eulerLabels module. A clean packed consumer with skipLibCheck: false therefore pulls in unrelated SDK declarations and fails on the existing src/utils/oracle.js package-internal import.
  4. Unvalidated product IDs are assigned into a plain {}. A remote product ID of __proto__ mutates the products record’s prototype, omits the product from enumeration, but still leaves its vault in verifiedVaultAddresses.
  5. Entity hydration keeps claiming new work after one worker fails and the caller has already received a rejection. In a 20-entity probe, 16 detail requests existed at rejection and all 40 were issued afterward.
  6. Referenced entity IDs are not bounded or required to exist in the fetched inventory. One product with 250 IDs and zero listed entities triggered 500 follow-up requests and returned 250 hydrated entities.
  7. Pagination trusts a changing meta.total; a 101→1 total change returned only the first record rather than rejecting the inconsistent immutable snapshot.
  8. The new open social index signature is source-incompatible for downstream keyof/indexed-access consumers; defillama should be added explicitly instead.
  9. All focused adapter tests inject a request double, leaving endpoint joining, query encoding, API-key headers, JSON handling, and HTTP failures in the built-in transport untested.

Inline comments contain the exact code paths and minimal discriminating tests.

Validation

  • pnpm --filter @eulerxyz/euler-v2-sdk exec vitest run test/publicLabelsV3.test.ts8/8 passed
  • pnpm --filter @eulerxyz/euler-v2-sdk test35 files / 519 tests passed
  • pnpm --filter @eulerxyz/euler-v2-sdk typecheck — passed
  • pnpm --filter @eulerxyz/euler-v2-sdk build — passed
  • pnpm --filter @eulerxyz/euler-v2-sdk lint — exit 0; five existing warnings outside this diff
  • synthetic packed-package runtime imports — root and public-labels subpath passed
  • synthetic packed-package strict declaration import — failed because the focused subpath reaches unrelated broad SDK declarations; inline finding added
  • live snapshot — resolved v20260804151305236; 921 vaults, 65 products, 35 entities, 44 entity addresses, 103 raw geo policies
  • live KPK/Securitize fixture — KPK remains owner, Securitize remains display-only co-brand, both hosted logos resolved
  • live integrity checks — no duplicate vault/product/entity keys, missing product/entity references, or current endpoint/row entity mismatches
  • exact Lite #779 with the branch-built SDK — typecheck, production build, focused 63/63, and full 1,602/1,602 tests passed

Cross-repo note

The linked Lite head consumes the new subpath correctly. Its stock GitHub build/typecheck currently fail because the committed published SDK does not contain the unpublished subpath; the Railway preview build with branch injection is green. That is an explicit publication/sequencing gate, not a separate defect in this SDK diff.

What looks good

  • latest is resolved once and all snapshot reads use the concrete publication key
  • pagination follows meta.total and fails on stalled pages
  • co-brand entities are kept out of the managing-entity field
  • geo policies remain raw/informational rather than being promoted into eligibility logic
  • no dependency, lockfile, workflow, secret, or unexpected executable-surface changes

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up challenge pass on unchanged head 18145a8738d852a93fe09cbbd57a108d4d65bd69. Four additional actionable findings survived independent reproduction: unbounded follow-up request amplification, inconsistent pagination totals returning incorrect data, a source-incompatible public social type, and the default HTTP transport being absent from tests. See inline comments for exact probes and minimal fixes.

...new Set([
...products.flatMap((product) => [
product.entityId,
...(product.coBrandEntityIds ?? []),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · blocking resource bound: mapper concurrency limits simultaneous work, but this expansion does not bound how much work is created. Every owner/co-brand/vault entity ID triggers two detail requests; coBrandEntityIds is unbounded, and referenced IDs are not required to exist in the fetched /entities inventory. I reproduced one product with 250 referenced IDs and an empty entity inventory: the adapter accepted it, issued 500 detail requests, and returned 250 hydrated entities. With up to 10,000 top-level products, malformed arrays can amplify far beyond the list cap. Before hydration, require referential integrity against the fetched entity inventory and cap both per-product co-brand IDs and total unique hydration IDs. Add a fixture proving an oversized/unlisted reference set rejects without issuing detail calls.

limit: PUBLIC_LABELS_PAGE_SIZE,
offset,
});
const { items, total } = assertListResponse(response, path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · immutable pagination integrity: each page's fresh meta.total controls termination and slice(). I reproduced page one returning 100 items with total=101, then page two returning one item with total=1; this function silently returned only [0] instead of rejecting the inconsistent snapshot. Capture the first page's total and require every later page to match it; also validate returned offset/limit against the request. Add changing-total and short non-final-page fixtures so immutable pagination cannot silently truncate or skip records.

discord: string;
telegram: string;
github: string;
[key: string]: string | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue · public type compatibility: this open index signature changes keyof EulerLabelEntity["social"] from the finite required-key union to string | number, and indexed access becomes string | undefined. A downstream generic fixture assigning social[keyof typeof social] to string compiles against the base type and fails on this head with TS2322. If the new V3 field is the goal, add defillama?: string explicitly instead of widening every social key, and retain a compile-time compatibility fixture.

},
});

const fixtureRequest = (options?: { productEntityId?: string }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test adequacy: every adapter test injects this request double, so none exercise buildPublicLabelsRequest. A concrete mutant changing X-API-Key to Authorization still passed all eight focused tests; wrong endpoint joining, query encoding, JSON handling, and non-2xx behavior would also survive. Add a stubbed-global-fetch test around the default adapter that asserts the final /v3 URL, version/chain/pagination query, X-API-Key, and HTTP error propagation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants