@adcp/sdk 10.0.0 → 11.0.0

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.
Files changed (48) hide show
  1. package/dist/lib/adapters/version/3.0/get-products.d.ts +3 -0
  2. package/dist/lib/adapters/version/3.0/get-products.d.ts.map +1 -0
  3. package/dist/lib/adapters/version/3.0/get-products.js +48 -0
  4. package/dist/lib/adapters/version/3.0/get-products.js.map +1 -0
  5. package/dist/lib/adapters/version/3.0/index.d.ts.map +1 -1
  6. package/dist/lib/adapters/version/3.0/index.js +2 -1
  7. package/dist/lib/adapters/version/3.0/index.js.map +1 -1
  8. package/dist/lib/brand/index.d.ts +32 -0
  9. package/dist/lib/brand/index.d.ts.map +1 -1
  10. package/dist/lib/brand/index.js +137 -0
  11. package/dist/lib/brand/index.js.map +1 -1
  12. package/dist/lib/discovery/validate-adagents.js +1 -1
  13. package/dist/lib/discovery/validate-adagents.js.map +1 -1
  14. package/dist/lib/index.d.ts +4 -4
  15. package/dist/lib/index.d.ts.map +1 -1
  16. package/dist/lib/index.js +15 -13
  17. package/dist/lib/index.js.map +1 -1
  18. package/dist/lib/registry/index.d.ts +6 -9
  19. package/dist/lib/registry/index.d.ts.map +1 -1
  20. package/dist/lib/registry/index.js.map +1 -1
  21. package/dist/lib/registry/property-registry.d.ts +6 -0
  22. package/dist/lib/registry/property-registry.d.ts.map +1 -1
  23. package/dist/lib/registry/property-registry.js +10 -1
  24. package/dist/lib/registry/property-registry.js.map +1 -1
  25. package/dist/lib/registry/sync.d.ts +77 -4
  26. package/dist/lib/registry/sync.d.ts.map +1 -1
  27. package/dist/lib/registry/sync.js +480 -64
  28. package/dist/lib/registry/sync.js.map +1 -1
  29. package/dist/lib/registry/types.d.ts +21 -2
  30. package/dist/lib/registry/types.d.ts.map +1 -1
  31. package/dist/lib/registry/types.generated.d.ts +4 -8
  32. package/dist/lib/registry/types.generated.d.ts.map +1 -1
  33. package/dist/lib/registry/types.generated.js +1 -1
  34. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  35. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  36. package/dist/lib/testing/storyboard/validations.js +43 -1
  37. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  38. package/dist/lib/utils/creative-adapter.d.ts +16 -10
  39. package/dist/lib/utils/creative-adapter.d.ts.map +1 -1
  40. package/dist/lib/utils/creative-adapter.js +40 -24
  41. package/dist/lib/utils/creative-adapter.js.map +1 -1
  42. package/dist/lib/utils/request-normalizer.d.ts +8 -1
  43. package/dist/lib/utils/request-normalizer.d.ts.map +1 -1
  44. package/dist/lib/utils/request-normalizer.js +14 -16
  45. package/dist/lib/utils/request-normalizer.js.map +1 -1
  46. package/dist/lib/version.d.ts +3 -3
  47. package/dist/lib/version.js +3 -3
  48. package/package.json +1 -1
@@ -56,7 +56,9 @@ class RegistrySync extends node_events_1.EventEmitter {
56
56
  maxStreamFailures;
57
57
  indexAgents;
58
58
  indexAuthorizations;
59
+ indexProperties;
59
60
  indexBrandHierarchies;
61
+ ignoredEventHandler;
60
62
  errorHandler;
61
63
  cursorStore;
62
64
  _state = 'idle';
@@ -78,6 +80,10 @@ class RegistrySync extends node_events_1.EventEmitter {
78
80
  agents = new Map();
79
81
  authByDomain = new Map();
80
82
  authByAgent = new Map();
83
+ authLocationsById = new Map();
84
+ propertiesByRid = new Map();
85
+ propertyRidsByDomain = new Map();
86
+ propertyAliasesByRid = new Map();
81
87
  brandAncestorsByDomain = new Map();
82
88
  brandHierarchyByDomain = new Map();
83
89
  brandHierarchyKeysByEntity = new Map();
@@ -106,8 +112,10 @@ class RegistrySync extends node_events_1.EventEmitter {
106
112
  this.maxStreamFailures = config.maxStreamFailures ?? 3;
107
113
  this.indexAgents = config.indexes?.agents !== false;
108
114
  this.indexAuthorizations = config.indexes?.authorizations !== false;
115
+ this.indexProperties = config.indexes?.properties !== false;
109
116
  this.indexBrandHierarchies = config.indexes?.brandHierarchies !== false;
110
117
  this.cursorStore = config.cursorStore ?? new cursor_store_1.InMemoryCursorStore();
118
+ this.ignoredEventHandler = config.onIgnoredEvent;
111
119
  this.errorHandler = config.onError;
112
120
  }
113
121
  // ====== Lifecycle ======
@@ -196,7 +204,7 @@ class RegistrySync extends node_events_1.EventEmitter {
196
204
  if (filter.min_properties != null && p.property_count < filter.min_properties)
197
205
  return false;
198
206
  if (filter.compliance_status?.length) {
199
- const status = agent.compliance_summary?.status ?? 'unknown';
207
+ const status = (agent.compliance_summary?.status ?? 'unknown');
200
208
  if (!filter.compliance_status.includes(status))
201
209
  return false;
202
210
  }
@@ -206,11 +214,18 @@ class RegistrySync extends node_events_1.EventEmitter {
206
214
  // ====== Authorization Lookups ======
207
215
  /** Get all authorizations for a publisher domain. */
208
216
  getAuthorizationsForDomain(domain) {
209
- return this.authByDomain.get(domain) ?? [];
217
+ return this.authByDomain.get(this.authDomainKey(domain)) ?? [];
210
218
  }
211
219
  /** Get all authorizations for an agent. */
212
220
  getAuthorizationsForAgent(agentUrl) {
213
- return this.authByAgent.get(agentUrl) ?? [];
221
+ const entries = [];
222
+ for (const key of this.authAgentKeysForUrl(agentUrl)) {
223
+ for (const entry of this.authByAgent.get(key) ?? []) {
224
+ if (!entries.includes(entry))
225
+ entries.push(entry);
226
+ }
227
+ }
228
+ return entries;
214
229
  }
215
230
  /**
216
231
  * Check if an agent has any authorization for a publisher domain.
@@ -223,8 +238,27 @@ class RegistrySync extends node_events_1.EventEmitter {
223
238
  * authorization after a missed revocation.
224
239
  */
225
240
  isAuthorized(agentUrl, domain) {
226
- const entries = this.authByDomain.get(domain);
227
- return entries != null && entries.some(e => e.agent_url === agentUrl);
241
+ const entries = this.authByDomain.get(this.authDomainKey(domain));
242
+ return entries != null && entries.some(entry => this.authorizationAgentMatches(entry, agentUrl));
243
+ }
244
+ // ====== Property Lookups ======
245
+ /** Get a property feed record by property_rid. Merged aliases resolve to their canonical RID. */
246
+ getProperty(rid) {
247
+ const property = this.propertiesByRid.get(this.resolvePropertyRid(rid));
248
+ return property ? this.cloneProperty(property) : undefined;
249
+ }
250
+ /** Get all property feed records for a publisher domain. */
251
+ getPropertiesForDomain(domain) {
252
+ const rids = this.propertyRidsByDomain.get(this.normalizeDomainKey(domain));
253
+ if (!rids)
254
+ return [];
255
+ const properties = [];
256
+ for (const rid of rids) {
257
+ const property = this.propertiesByRid.get(this.resolvePropertyRid(rid));
258
+ if (property)
259
+ properties.push(this.cloneProperty(property));
260
+ }
261
+ return properties;
228
262
  }
229
263
  // ====== Brand Hierarchy Lookups ======
230
264
  /**
@@ -254,6 +288,7 @@ class RegistrySync extends node_events_1.EventEmitter {
254
288
  return {
255
289
  agents: this.agents.size,
256
290
  authorizations: authCount,
291
+ properties: this.propertiesByRid.size,
257
292
  brandHierarchies: this.brandHierarchyKeysByEntity.size,
258
293
  };
259
294
  }
@@ -694,37 +729,29 @@ class RegistrySync extends node_events_1.EventEmitter {
694
729
  }
695
730
  // ====== Private: Event Application ======
696
731
  applyEvent(event) {
697
- const payload = event.payload;
732
+ const payload = this.asRecord(event.payload);
698
733
  switch (event.event_type) {
699
734
  case 'agent.discovered':
700
735
  case 'agent.profile_updated': {
701
736
  if (!this.indexAgents)
702
737
  break;
703
738
  const existing = this.agents.get(event.entity_id);
704
- if (existing && payload.inventory_profile) {
739
+ const inventoryProfile = this.isAgentInventoryProfile(payload.inventory_profile)
740
+ ? payload.inventory_profile
741
+ : undefined;
742
+ if (existing && inventoryProfile) {
705
743
  this.agents.set(event.entity_id, {
706
744
  ...existing,
707
- inventory_profile: payload.inventory_profile,
745
+ inventory_profile: inventoryProfile,
708
746
  });
709
747
  }
710
748
  else if (!existing) {
711
749
  // New agent: use payload data or create stub
712
750
  this.agents.set(event.entity_id, {
713
751
  url: event.entity_id,
714
- name: payload.name ?? event.entity_id,
715
- type: payload.type ?? 'unknown',
716
- inventory_profile: payload.inventory_profile ?? {
717
- channels: [],
718
- property_types: [],
719
- markets: [],
720
- categories: [],
721
- category_taxonomy: 'iab_content_3.0',
722
- tags: [],
723
- delivery_types: [],
724
- property_count: 0,
725
- publisher_count: 0,
726
- has_tmp: false,
727
- },
752
+ name: typeof payload.name === 'string' ? payload.name : event.entity_id,
753
+ type: typeof payload.type === 'string' ? payload.type : 'unknown',
754
+ inventory_profile: inventoryProfile ?? this.emptyInventoryProfile(),
728
755
  match: { score: 0, matched_filters: [] },
729
756
  });
730
757
  }
@@ -734,62 +761,47 @@ class RegistrySync extends node_events_1.EventEmitter {
734
761
  if (this.indexAgents)
735
762
  this.agents.delete(event.entity_id);
736
763
  if (this.indexAuthorizations)
737
- this.authByAgent.delete(event.entity_id);
764
+ this.removeAuthorizationsForAgent(event.entity_id);
738
765
  break;
739
766
  }
740
767
  case 'authorization.granted': {
741
768
  if (!this.indexAuthorizations)
742
769
  break;
743
- const entry = payload;
744
- if (!entry.agent_url || !entry.publisher_domain || !entry.authorization_type)
770
+ if (!this.isAuthorizationPayload(payload))
745
771
  break;
746
- const domainEntries = this.authByDomain.get(entry.publisher_domain) ?? [];
747
- if (!domainEntries.some(e => e.agent_url === entry.agent_url && e.authorization_type === entry.authorization_type)) {
748
- domainEntries.push(entry);
749
- this.authByDomain.set(entry.publisher_domain, domainEntries);
750
- }
751
- const agentEntries = this.authByAgent.get(entry.agent_url) ?? [];
752
- if (!agentEntries.some(e => e.publisher_domain === entry.publisher_domain && e.authorization_type === entry.authorization_type)) {
753
- agentEntries.push(entry);
754
- this.authByAgent.set(entry.agent_url, agentEntries);
755
- }
772
+ if (typeof payload.authorization_type !== 'string')
773
+ break;
774
+ this.upsertAuthorizationEntry(this.authorizationEntryFromEvent(payload));
775
+ break;
776
+ }
777
+ case 'authorization.modified': {
778
+ if (!this.indexAuthorizations)
779
+ break;
780
+ if (!this.isAuthorizationPayload(payload))
781
+ break;
782
+ const entry = this.authorizationEntryFromEvent(payload);
783
+ if (!entry.id && !entry.authorization_type)
784
+ break;
785
+ this.upsertAuthorizationEntry(entry, { fallbackToTupleOnMissingRowId: true });
756
786
  break;
757
787
  }
758
788
  case 'authorization.revoked': {
759
789
  if (!this.indexAuthorizations)
760
790
  break;
761
- const agentUrl = payload.agent_url;
762
- const domain = payload.publisher_domain;
763
- const authType = payload.authorization_type;
764
- if (!agentUrl || !domain)
791
+ if (!this.isAuthorizationPayload(payload))
765
792
  break;
766
- const domainEntries = this.authByDomain.get(domain);
767
- if (domainEntries) {
768
- const filtered = domainEntries.filter(e => !(e.agent_url === agentUrl && (!authType || e.authorization_type === authType)));
769
- if (filtered.length > 0)
770
- this.authByDomain.set(domain, filtered);
771
- else
772
- this.authByDomain.delete(domain);
773
- }
774
- const agentEntries = this.authByAgent.get(agentUrl);
775
- if (agentEntries) {
776
- const filtered = agentEntries.filter(e => !(e.publisher_domain === domain && (!authType || e.authorization_type === authType)));
777
- if (filtered.length > 0)
778
- this.authByAgent.set(agentUrl, filtered);
779
- else
780
- this.authByAgent.delete(agentUrl);
781
- }
793
+ const entry = this.authorizationEntryFromEvent(payload);
794
+ this.removeAuthorizationEntries(entry, {
795
+ matchByRowId: Boolean(entry.id || !entry.authorization_type),
796
+ fallbackToTupleOnMissingRowId: true,
797
+ });
782
798
  break;
783
799
  }
784
800
  case 'agent.compliance_changed': {
785
801
  if (this.indexAgents) {
786
802
  const existing = this.agents.get(event.entity_id);
787
803
  const summary = payload.compliance_summary;
788
- if (existing &&
789
- summary &&
790
- typeof summary === 'object' &&
791
- 'status' in summary &&
792
- typeof summary.status === 'string') {
804
+ if (existing && this.isAgentCompliance(summary)) {
793
805
  this.agents.set(event.entity_id, {
794
806
  ...existing,
795
807
  compliance_summary: summary,
@@ -807,6 +819,19 @@ class RegistrySync extends node_events_1.EventEmitter {
807
819
  });
808
820
  break;
809
821
  }
822
+ case 'property.created':
823
+ case 'property.updated':
824
+ case 'property.reactivated':
825
+ case 'property.stale': {
826
+ if (this.indexProperties)
827
+ this.upsertPropertyEvent(payload);
828
+ break;
829
+ }
830
+ case 'property.merged': {
831
+ if (this.indexProperties)
832
+ this.handlePropertyMerge(payload);
833
+ break;
834
+ }
810
835
  case 'brand.hierarchy_updated':
811
836
  case 'brand.updated':
812
837
  case 'brand.resolved': {
@@ -820,8 +845,20 @@ class RegistrySync extends node_events_1.EventEmitter {
820
845
  this.deleteBrandHierarchy(event.entity_id, payload);
821
846
  break;
822
847
  }
823
- // Property and publisher events: no-op for v1 (no property index yet)
848
+ case 'collection.created':
849
+ case 'collection.updated':
850
+ case 'collection.merged':
851
+ case 'collection.removed': {
852
+ this.ignoreEvent(event, 'collection.* indexing requires typed collection schemas and a reference consumer');
853
+ break;
854
+ }
855
+ case 'publisher.adagents_changed':
856
+ case 'publisher.adagents_discovered': {
857
+ this.ignoreEvent(event, 'publisher.* indexing requires a reference consumer');
858
+ break;
859
+ }
824
860
  default:
861
+ this.ignoreEvent(event, 'unknown or unsupported registry feed event type');
825
862
  break;
826
863
  }
827
864
  }
@@ -830,11 +867,301 @@ class RegistrySync extends node_events_1.EventEmitter {
830
867
  this.agents.clear();
831
868
  this.authByDomain.clear();
832
869
  this.authByAgent.clear();
870
+ this.authLocationsById.clear();
871
+ this.propertiesByRid.clear();
872
+ this.propertyRidsByDomain.clear();
873
+ this.propertyAliasesByRid.clear();
833
874
  this.brandAncestorsByDomain.clear();
834
875
  this.brandHierarchyByDomain.clear();
835
876
  this.brandHierarchyKeysByEntity.clear();
836
877
  this.brandHierarchyEntityByKey.clear();
837
878
  }
879
+ ignoreEvent(event, reason) {
880
+ this.ignoredEventHandler?.(event, reason);
881
+ this.emit('ignoredEvent', { event, reason });
882
+ }
883
+ upsertAuthorizationEntry(entry, options) {
884
+ const normalizedEntry = this.normalizeAuthorizationEntry(entry);
885
+ this.removeAuthorizationEntries(normalizedEntry, {
886
+ matchByRowId: Boolean(normalizedEntry.id),
887
+ fallbackToTupleOnMissingRowId: options?.fallbackToTupleOnMissingRowId,
888
+ });
889
+ const domainKey = this.authDomainKey(normalizedEntry.publisher_domain);
890
+ const domainEntries = this.authByDomain.get(domainKey) ?? [];
891
+ domainEntries.push(normalizedEntry);
892
+ this.authByDomain.set(domainKey, domainEntries);
893
+ const agentKey = this.authAgentKey(normalizedEntry);
894
+ for (const key of this.authAgentKeys(normalizedEntry)) {
895
+ const agentEntries = this.authByAgent.get(key) ?? [];
896
+ agentEntries.push(normalizedEntry);
897
+ this.authByAgent.set(key, agentEntries);
898
+ }
899
+ if (normalizedEntry.id) {
900
+ this.authLocationsById.set(normalizedEntry.id, {
901
+ agentKey,
902
+ domainKey,
903
+ });
904
+ }
905
+ }
906
+ normalizeAuthorizationEntry(entry) {
907
+ if (entry.effective_until || !entry.effective_to)
908
+ return entry;
909
+ return { ...entry, effective_until: entry.effective_to };
910
+ }
911
+ authorizationEntryFromEvent(entry) {
912
+ return this.normalizeAuthorizationEntry(entry);
913
+ }
914
+ removeAuthorizationsForAgent(agentUrl) {
915
+ const agentKeys = this.authAgentKeysForUrl(agentUrl);
916
+ for (const [key, entries] of this.authByAgent) {
917
+ if (entries.some(entry => this.authorizationAgentMatches(entry, agentUrl)))
918
+ agentKeys.add(key);
919
+ }
920
+ for (const agentKey of agentKeys) {
921
+ const entries = [...(this.authByAgent.get(agentKey) ?? [])];
922
+ for (const entry of entries) {
923
+ this.removeAuthorizationEntries(entry, {
924
+ matchByRowId: Boolean(entry.id),
925
+ fallbackToTupleOnMissingRowId: true,
926
+ });
927
+ }
928
+ this.authByAgent.delete(agentKey);
929
+ }
930
+ }
931
+ removeAuthorizationEntries(target, options) {
932
+ const normalizedTarget = this.normalizeAuthorizationEntry(target);
933
+ const domainKeys = new Set([this.authDomainKey(normalizedTarget.publisher_domain)]);
934
+ const agentKeys = this.authAgentKeys(normalizedTarget);
935
+ let effectiveOptions = options;
936
+ if (normalizedTarget.id && options?.matchByRowId) {
937
+ const existingLocation = this.authLocationsById.get(normalizedTarget.id);
938
+ if (!existingLocation && options.fallbackToTupleOnMissingRowId) {
939
+ effectiveOptions = { ...options, matchByRowId: false };
940
+ }
941
+ if (existingLocation) {
942
+ domainKeys.add(existingLocation.domainKey);
943
+ agentKeys.add(existingLocation.agentKey);
944
+ }
945
+ }
946
+ for (const domain of domainKeys) {
947
+ const entries = this.authByDomain.get(domain);
948
+ if (!entries)
949
+ continue;
950
+ for (const entry of entries) {
951
+ if (this.authorizationMatches(entry, normalizedTarget, effectiveOptions)) {
952
+ for (const key of this.authAgentKeys(entry))
953
+ agentKeys.add(key);
954
+ }
955
+ }
956
+ }
957
+ for (const domain of domainKeys) {
958
+ this.removeAuthorizationEntriesFromBucket(this.authByDomain, domain, normalizedTarget, effectiveOptions);
959
+ }
960
+ for (const agentUrl of agentKeys) {
961
+ this.removeAuthorizationEntriesFromBucket(this.authByAgent, agentUrl, normalizedTarget, effectiveOptions);
962
+ }
963
+ }
964
+ removeAuthorizationEntriesFromBucket(bucket, key, target, options) {
965
+ const entries = bucket.get(key);
966
+ if (!entries)
967
+ return;
968
+ const filtered = [];
969
+ for (const entry of entries) {
970
+ if (this.authorizationMatches(entry, target, options)) {
971
+ if (entry.id)
972
+ this.authLocationsById.delete(entry.id);
973
+ }
974
+ else {
975
+ filtered.push(entry);
976
+ }
977
+ }
978
+ if (filtered.length > 0)
979
+ bucket.set(key, filtered);
980
+ else
981
+ bucket.delete(key);
982
+ }
983
+ authorizationMatches(entry, target, options) {
984
+ if (target.id && options?.matchByRowId)
985
+ return entry.id === target.id;
986
+ if (!this.authorizationEntriesShareAgent(entry, target))
987
+ return false;
988
+ if (this.authDomainKey(entry.publisher_domain) !== this.authDomainKey(target.publisher_domain))
989
+ return false;
990
+ if (target.authorization_type)
991
+ return entry.authorization_type === target.authorization_type;
992
+ return true;
993
+ }
994
+ authorizationAgentMatches(entry, agentUrl) {
995
+ return this.authAgentKeysOverlap(this.authAgentKeys(entry), this.authAgentKeysForUrl(agentUrl));
996
+ }
997
+ authorizationEntriesShareAgent(left, right) {
998
+ return this.authAgentKeysOverlap(this.authAgentKeys(left), this.authAgentKeys(right));
999
+ }
1000
+ authDomainKey(domain) {
1001
+ return this.normalizeDomainKey(domain);
1002
+ }
1003
+ authAgentKey(entry) {
1004
+ return this.authAgentKeyFromUrl(entry.agent_url);
1005
+ }
1006
+ authAgentKeys(entry) {
1007
+ const keys = this.authAgentKeysForUrl(entry.agent_url);
1008
+ if (entry.agent_url_canonical?.trim()) {
1009
+ for (const key of this.authAgentKeysForUrl(entry.agent_url_canonical))
1010
+ keys.add(key);
1011
+ }
1012
+ return keys;
1013
+ }
1014
+ authAgentKeysForUrl(agentUrl) {
1015
+ const trimmed = agentUrl.trim();
1016
+ const keys = new Set();
1017
+ if (trimmed)
1018
+ keys.add(trimmed);
1019
+ keys.add(this.authAgentKeyFromUrl(agentUrl));
1020
+ return keys;
1021
+ }
1022
+ authAgentKeysOverlap(left, right) {
1023
+ for (const key of left) {
1024
+ if (right.has(key))
1025
+ return true;
1026
+ }
1027
+ return false;
1028
+ }
1029
+ authAgentKeyFromUrl(agentUrl) {
1030
+ const trimmed = agentUrl.trim();
1031
+ if (!trimmed)
1032
+ return trimmed;
1033
+ try {
1034
+ const parsed = new URL(trimmed);
1035
+ if (parsed.host)
1036
+ return `${parsed.protocol.toLowerCase()}//${parsed.host.toLowerCase()}`;
1037
+ }
1038
+ catch {
1039
+ // Fall through to best-effort normalization for malformed or scheme-less values.
1040
+ }
1041
+ const schemeEnd = trimmed.indexOf('://');
1042
+ const authorityStart = schemeEnd > 0 ? schemeEnd + 3 : 0;
1043
+ let authorityEnd = trimmed.length;
1044
+ for (const delimiter of ['/', '?', '#']) {
1045
+ const index = trimmed.indexOf(delimiter, authorityStart);
1046
+ if (index >= 0 && index < authorityEnd)
1047
+ authorityEnd = index;
1048
+ }
1049
+ if (authorityEnd === authorityStart)
1050
+ return trimmed.toLowerCase();
1051
+ const scheme = schemeEnd > 0 ? `${trimmed.slice(0, authorityStart).toLowerCase()}` : '';
1052
+ return `${scheme}${trimmed.slice(authorityStart, authorityEnd).toLowerCase()}`;
1053
+ }
1054
+ upsertPropertyEvent(payload) {
1055
+ const rid = this.propertyRidFromPayload(payload);
1056
+ if (!rid)
1057
+ return;
1058
+ const canonicalRid = this.resolvePropertyRid(rid);
1059
+ const existing = this.propertiesByRid.get(canonicalRid);
1060
+ const next = this.propertyFromPayload(canonicalRid, payload, existing);
1061
+ if (!next)
1062
+ return;
1063
+ this.setPropertyEntry(next, existing);
1064
+ }
1065
+ handlePropertyMerge(payload) {
1066
+ if (typeof payload.alias_rid !== 'string' || typeof payload.canonical_rid !== 'string')
1067
+ return;
1068
+ const aliasRid = this.resolvePropertyRid(payload.alias_rid);
1069
+ const canonicalRid = this.resolvePropertyRid(payload.canonical_rid);
1070
+ if (!aliasRid || !canonicalRid || aliasRid === canonicalRid)
1071
+ return;
1072
+ const aliasEntry = this.propertiesByRid.get(aliasRid);
1073
+ const canonicalEntry = this.propertiesByRid.get(canonicalRid);
1074
+ const payloadEntry = this.propertyFromPayload(canonicalRid, payload);
1075
+ this.propertyAliasesByRid.set(payload.alias_rid, canonicalRid);
1076
+ this.propertyAliasesByRid.set(aliasRid, canonicalRid);
1077
+ if (!aliasEntry && !canonicalEntry && !payloadEntry)
1078
+ return;
1079
+ const merged = {
1080
+ ...(aliasEntry ? this.cloneProperty(aliasEntry) : {}),
1081
+ ...(canonicalEntry ? this.cloneProperty(canonicalEntry) : {}),
1082
+ ...(payloadEntry ? this.cloneProperty(payloadEntry) : {}),
1083
+ property_rid: canonicalRid,
1084
+ };
1085
+ if (!merged.publisher_domain) {
1086
+ merged.publisher_domain = canonicalEntry?.publisher_domain ?? aliasEntry?.publisher_domain;
1087
+ }
1088
+ this.deletePropertyEntry(aliasRid);
1089
+ this.setPropertyEntry(merged, canonicalEntry);
1090
+ }
1091
+ propertyFromPayload(rid, payload, existing) {
1092
+ const publisherDomain = this.propertyDomainFromPayload(payload) ?? existing?.publisher_domain;
1093
+ const next = existing ? this.cloneProperty(existing) : { property_rid: rid };
1094
+ let hasPayloadFields = false;
1095
+ for (const [key, value] of Object.entries(payload)) {
1096
+ if (value === undefined || key === 'alias_rid' || key === 'canonical_rid')
1097
+ continue;
1098
+ hasPayloadFields = true;
1099
+ next[key] = this.cloneJsonValue(value);
1100
+ }
1101
+ next.property_rid = rid;
1102
+ if (publisherDomain)
1103
+ next.publisher_domain = publisherDomain;
1104
+ return hasPayloadFields || publisherDomain || existing ? next : null;
1105
+ }
1106
+ setPropertyEntry(entry, previous) {
1107
+ if (previous?.publisher_domain && previous.publisher_domain !== entry.publisher_domain) {
1108
+ this.removePropertyRidFromDomain(entry.property_rid, previous.publisher_domain);
1109
+ }
1110
+ this.propertiesByRid.set(entry.property_rid, this.cloneProperty(entry));
1111
+ if (entry.publisher_domain) {
1112
+ const key = this.normalizeDomainKey(entry.publisher_domain);
1113
+ const rids = this.propertyRidsByDomain.get(key) ?? new Set();
1114
+ rids.add(entry.property_rid);
1115
+ this.propertyRidsByDomain.set(key, rids);
1116
+ }
1117
+ }
1118
+ deletePropertyEntry(rid) {
1119
+ const entry = this.propertiesByRid.get(rid);
1120
+ if (entry?.publisher_domain)
1121
+ this.removePropertyRidFromDomain(rid, entry.publisher_domain);
1122
+ this.propertiesByRid.delete(rid);
1123
+ }
1124
+ removePropertyRidFromDomain(rid, domain) {
1125
+ const key = this.normalizeDomainKey(domain);
1126
+ const rids = this.propertyRidsByDomain.get(key);
1127
+ if (!rids)
1128
+ return;
1129
+ rids.delete(rid);
1130
+ if (rids.size === 0)
1131
+ this.propertyRidsByDomain.delete(key);
1132
+ }
1133
+ propertyRidFromPayload(payload) {
1134
+ if (typeof payload.property_rid === 'string' && payload.property_rid.trim())
1135
+ return payload.property_rid;
1136
+ if (typeof payload.rid === 'string' && payload.rid.trim())
1137
+ return payload.rid;
1138
+ if (typeof payload.id === 'string' && payload.id.trim())
1139
+ return payload.id;
1140
+ return null;
1141
+ }
1142
+ propertyDomainFromPayload(payload) {
1143
+ if (typeof payload.publisher_domain === 'string' && payload.publisher_domain.trim())
1144
+ return payload.publisher_domain;
1145
+ const property = this.asRecord(payload.property);
1146
+ return typeof property.publisher_domain === 'string' && property.publisher_domain.trim()
1147
+ ? property.publisher_domain
1148
+ : undefined;
1149
+ }
1150
+ stringValue(value) {
1151
+ return typeof value === 'string' && value.trim() ? value : undefined;
1152
+ }
1153
+ resolvePropertyRid(rid) {
1154
+ let current = rid;
1155
+ const seen = new Set();
1156
+ while (this.propertyAliasesByRid.has(current) && !seen.has(current)) {
1157
+ seen.add(current);
1158
+ current = this.propertyAliasesByRid.get(current);
1159
+ }
1160
+ return current;
1161
+ }
1162
+ cloneProperty(property) {
1163
+ return this.cloneJsonValue(property);
1164
+ }
838
1165
  applyBrandHierarchyEvent(entityId, payload) {
839
1166
  const resolvedChain = this.extractResolvedBrandChain(payload.chain);
840
1167
  const domainChain = resolvedChain.length > 0
@@ -906,12 +1233,101 @@ class RegistrySync extends node_events_1.EventEmitter {
906
1233
  }
907
1234
  this.brandHierarchyKeysByEntity.delete(entityKey);
908
1235
  }
1236
+ asRecord(value) {
1237
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1238
+ }
1239
+ emptyInventoryProfile() {
1240
+ return {
1241
+ channels: [],
1242
+ property_types: [],
1243
+ markets: [],
1244
+ categories: [],
1245
+ category_taxonomy: 'iab_content_3.0',
1246
+ tags: [],
1247
+ delivery_types: [],
1248
+ property_count: 0,
1249
+ publisher_count: 0,
1250
+ has_tmp: false,
1251
+ };
1252
+ }
1253
+ isStringArray(value) {
1254
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
1255
+ }
1256
+ isAgentInventoryProfile(value) {
1257
+ const profile = this.asRecord(value);
1258
+ return (this.isStringArray(profile.channels) &&
1259
+ this.isStringArray(profile.property_types) &&
1260
+ this.isStringArray(profile.markets) &&
1261
+ this.isStringArray(profile.categories) &&
1262
+ (typeof profile.category_taxonomy === 'string' || profile.category_taxonomy === null) &&
1263
+ this.isStringArray(profile.tags) &&
1264
+ this.isStringArray(profile.delivery_types) &&
1265
+ (profile.format_ids === undefined || this.isStringArray(profile.format_ids)) &&
1266
+ typeof profile.property_count === 'number' &&
1267
+ typeof profile.publisher_count === 'number' &&
1268
+ typeof profile.has_tmp === 'boolean');
1269
+ }
1270
+ isAuthorizationPayload(value) {
1271
+ const payload = this.asRecord(value);
1272
+ return ((payload.id === undefined || this.stringValue(payload.id) !== undefined) &&
1273
+ this.stringValue(payload.agent_url) !== undefined &&
1274
+ this.stringValue(payload.publisher_domain) !== undefined &&
1275
+ (payload.authorization_type === undefined || this.stringValue(payload.authorization_type) !== undefined) &&
1276
+ (payload.property_ids === undefined || this.isStringArray(payload.property_ids)) &&
1277
+ (payload.property_tags === undefined || this.isStringArray(payload.property_tags)) &&
1278
+ (payload.placement_ids === undefined || this.isStringArray(payload.placement_ids)) &&
1279
+ (payload.placement_tags === undefined || this.isStringArray(payload.placement_tags)) &&
1280
+ (payload.countries === undefined || this.isStringArray(payload.countries)) &&
1281
+ (payload.effective_from === undefined || typeof payload.effective_from === 'string') &&
1282
+ (payload.effective_until === undefined || typeof payload.effective_until === 'string') &&
1283
+ (payload.effective_to === undefined || typeof payload.effective_to === 'string'));
1284
+ }
1285
+ isAgentCompliance(value) {
1286
+ const summary = this.asRecord(value);
1287
+ const statuses = new Set(['passing', 'degraded', 'failing', 'unknown', 'opted_out']);
1288
+ const lifecycleStages = new Set(['development', 'testing', 'production', 'deprecated']);
1289
+ if (summary.status === 'opted_out') {
1290
+ return ((summary.lifecycle_stage === undefined ||
1291
+ (typeof summary.lifecycle_stage === 'string' && lifecycleStages.has(summary.lifecycle_stage))) &&
1292
+ (summary.tracks === undefined ||
1293
+ (summary.tracks != null && typeof summary.tracks === 'object' && !Array.isArray(summary.tracks))) &&
1294
+ (summary.streak_days === undefined || typeof summary.streak_days === 'number') &&
1295
+ (summary.last_checked_at === undefined ||
1296
+ typeof summary.last_checked_at === 'string' ||
1297
+ summary.last_checked_at === null) &&
1298
+ (summary.headline === undefined || typeof summary.headline === 'string' || summary.headline === null));
1299
+ }
1300
+ return (typeof summary.status === 'string' &&
1301
+ statuses.has(summary.status) &&
1302
+ typeof summary.lifecycle_stage === 'string' &&
1303
+ lifecycleStages.has(summary.lifecycle_stage) &&
1304
+ summary.tracks != null &&
1305
+ typeof summary.tracks === 'object' &&
1306
+ !Array.isArray(summary.tracks) &&
1307
+ typeof summary.streak_days === 'number' &&
1308
+ (typeof summary.last_checked_at === 'string' || summary.last_checked_at === null) &&
1309
+ (typeof summary.headline === 'string' || summary.headline === null));
1310
+ }
1311
+ isResolvedBrand(value) {
1312
+ const brand = this.asRecord(value);
1313
+ return typeof brand.canonical_id === 'string' && typeof brand.canonical_domain === 'string';
1314
+ }
1315
+ cloneJsonValue(value) {
1316
+ if (Array.isArray(value))
1317
+ return value.map(item => this.cloneJsonValue(item));
1318
+ if (value && typeof value === 'object') {
1319
+ const copy = {};
1320
+ for (const [key, nested] of Object.entries(value)) {
1321
+ copy[key] = this.cloneJsonValue(nested);
1322
+ }
1323
+ return copy;
1324
+ }
1325
+ return value;
1326
+ }
909
1327
  extractResolvedBrandChain(value) {
910
1328
  if (!Array.isArray(value))
911
1329
  return [];
912
- if (!value.every(item => item && typeof item === 'object' && !Array.isArray(item)))
913
- return [];
914
- return value.filter(item => typeof item.canonical_domain === 'string');
1330
+ return value.filter(item => this.isResolvedBrand(item));
915
1331
  }
916
1332
  extractDomainChain(value) {
917
1333
  if (!Array.isArray(value))