• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / 22148703734

18 Feb 2026 04:41PM UTC coverage: 96.218% (+0.01%) from 96.205%
22148703734

Pull #14488

github

ff15b7
patmmccann
Docs: require TypeScript for new src/modules/libraries files
Pull Request #14488: Prebid 11: document TypeScript-first policy for new source files

54237 of 66588 branches covered (81.45%)

208182 of 216366 relevant lines covered (96.22%)

69.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.51
/modules/storageControl.ts
1
import {config} from '../src/config.js';
1✔
2
import {metadata} from '../libraries/metadata/metadata.js';
3
import {
4
  ACTIVITY_PARAM_COMPONENT,
5
  ACTIVITY_PARAM_COMPONENT_NAME,
6
  ACTIVITY_PARAM_COMPONENT_TYPE,
7
  ACTIVITY_PARAM_STORAGE_KEY,
8
  ACTIVITY_PARAM_STORAGE_TYPE
9
} from '../src/activities/params.js';
10
import {
11
  discloseStorageUse,
12
  STORAGE_TYPE_COOKIES,
13
  STORAGE_TYPE_LOCALSTORAGE,
14
  type StorageDisclosure as Disclosure
15
} from '../src/storageManager.js';
16
import {logWarn, uniques} from '../src/utils.js';
17
import {registerActivityControl} from '../src/activities/rules.js';
18
import {ACTIVITY_ACCESS_DEVICE} from '../src/activities/activities.js';
19
import {addApiMethod} from "../src/prebid.ts";
20
// @ts-expect-error the ts compiler is confused by build-time renaming of summary.mjs to summary.js, reassure it
21
// eslint-disable-next-line prebid/validate-imports
22
import {getStorageDisclosureSummary} from "../libraries/storageDisclosure/summary.js";
23
import {getGlobal} from "../src/prebidGlobal.ts";
24

25
export const ENFORCE_STRICT = 'strict';
1✔
26
export const ENFORCE_ALIAS = 'allowAliases';
1✔
27
export const ENFORCE_OFF = 'off';
1✔
28

29
let enforcement;
30

31
function escapeRegExp(string) {
32
  return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
21✔
33
}
34

35
function matches(params, disclosure) {
36
  if (
28✔
37
    !['cookie', 'web'].includes(disclosure.type) ||
91✔
38
    (disclosure.type === 'cookie' && params[ACTIVITY_PARAM_STORAGE_TYPE] !== STORAGE_TYPE_COOKIES) ||
39
    (disclosure.type === 'web' && params[ACTIVITY_PARAM_STORAGE_TYPE] !== STORAGE_TYPE_LOCALSTORAGE)
40
  ) return false;
14✔
41
  const pattern = new RegExp(`^${disclosure.identifier.split('*').map(escapeRegExp).join('.*?')}$`);
14✔
42
  return pattern.test(params[ACTIVITY_PARAM_STORAGE_KEY]);
14✔
43
}
44

45
export function getDisclosures(params, meta = metadata) {
9!
46
  const matchingDisclosures = [];
9✔
47
  const disclosureURLs = {};
9✔
48
  const data = meta.getMetadata(params[ACTIVITY_PARAM_COMPONENT_TYPE], params[ACTIVITY_PARAM_COMPONENT_NAME]);
9✔
49
  if (!data) return null;
9✔
50
  disclosureURLs[params[ACTIVITY_PARAM_COMPONENT_NAME]] = data.disclosureURL;
8✔
51
  if (data.aliasOf) {
8✔
52
    const parent = meta.getMetadata(params[ACTIVITY_PARAM_COMPONENT_TYPE], data.aliasOf);
1✔
53
    if (parent) {
1✔
54
      disclosureURLs[data.aliasOf] = parent.disclosureURL;
1✔
55
    }
56
  }
57
  Object.entries(disclosureURLs).forEach(([componentName, disclosureURL]) => {
9✔
58
    meta.getStorageDisclosure(disclosureURL)
9✔
59
      ?.disclosures
60
      ?.filter(disclosure => matches(params, disclosure))
28✔
61
      ?.forEach(disclosure => {
62
        matchingDisclosures.push({
5✔
63
          [ACTIVITY_PARAM_COMPONENT_NAME]: componentName,
64
          disclosureURL,
65
          disclosure
66
        })
67
      })
68
  })
69
  return {
8✔
70
    matches: matchingDisclosures,
71
    disclosureURLs
72
  };
73
}
74

75
export function checkDisclosure(params, getMatchingDisclosures = getDisclosures) {
2!
76
  let disclosed = false;
2✔
77
  let parent = false;
2✔
78
  let reason = null;
2✔
79
  const key = params[ACTIVITY_PARAM_STORAGE_KEY]
2✔
80
  const component = params[ACTIVITY_PARAM_COMPONENT];
2✔
81
  if (key) {
2✔
82
    const disclosures = getMatchingDisclosures(params);
1✔
83
    if (disclosures == null) {
1!
84
      reason = `Cannot determine if storage key "${key}" is disclosed by "${component}" because the necessary metadata is missing - was it included in the build?`
×
85
    } else {
86
      const {disclosureURLs, matches} = disclosures;
1✔
87
      const moduleName = params[ACTIVITY_PARAM_COMPONENT_NAME]
1✔
88
      for (const {componentName} of matches) {
1✔
89
        if (componentName === moduleName) {
1!
90
          disclosed = true;
1✔
91
        } else {
92
          parent = true;
×
93
          reason = `Storage key "${key}" is disclosed by module "${componentName}", but not by "${moduleName}" itself (the latter is an alias of the former)`
×
94
        }
95
        if (disclosed || parent) break;
1!
96
      }
97
      if (!disclosed && !parent) {
1!
98
        reason = `Storage key "${key}" (for ${params[ACTIVITY_PARAM_STORAGE_TYPE]} storage) is not disclosed by "${component}"`
×
99
        if (disclosureURLs[moduleName]) {
×
100
          reason += ` @ ${disclosureURLs[moduleName]}`
×
101
        } else {
102
          reason += ` - no disclosure URL was provided, or it could not be retrieved`
×
103
        }
104
      }
105
    }
106
  } else {
107
    disclosed = null;
1✔
108
  }
109
  return {
2✔
110
    disclosed, parent, reason
111
  }
112
}
113

114
export function storageControlRule(getEnforcement = () => enforcement, check = checkDisclosure) {
7✔
115
  return function (params) {
7✔
116
    const {disclosed, parent, reason} = check(params);
6✔
117
    if (disclosed === null) return;
6✔
118
    if (!disclosed) {
5✔
119
      const enforcement = getEnforcement() ?? ENFORCE_STRICT;
4✔
120
      if (enforcement === ENFORCE_STRICT || (enforcement === ENFORCE_ALIAS && !parent)) return {allow: false, reason};
4✔
121
      if (reason) {
2✔
122
        logWarn('storageControl:', reason);
1✔
123
      }
124
    }
125
  }
126
}
127

128
const rule = registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'storageControl', storageControlRule());
1✔
129

130
export function deactivate() {
131
  // turn off this module; should only be used in testing
132
  rule();
1✔
133
}
134

135
export type StorageControlConfig = {
136
  /**
137
   * - 'strict': deny access to undisclosed storage keys (default)
138
   * - 'allowAliases': deny access to undisclosed storage keys, unless the use is from an alias of a module that does
139
   *    disclose them
140
   * - 'off': logs a warning when an undisclosed storage key is used
141
   */
142
  enforcement?: typeof ENFORCE_OFF | typeof ENFORCE_ALIAS | typeof ENFORCE_STRICT;
143
}
144

145
declare module '../src/config' {
146
  interface Config {
147
    storageControl: StorageControlConfig
148
  }
149
}
150

151
config.getConfig('storageControl', (cfg) => {
1✔
152
  enforcement = cfg?.storageControl?.enforcement ?? ENFORCE_STRICT;
×
153
})
154

155
export function dynamicDisclosureCollector() {
156
  const disclosures = {};
5✔
157
  function mergeDisclosures(left, right) {
158
    const merged = {
217✔
159
      ...left,
160
      purposes: (left.purposes ?? []).concat(right.purposes ?? []).filter(uniques),
1,302!
161
    };
162
    if (left.type === 'cookie') {
217✔
163
      if (left.maxAgeSeconds != null || right.maxAgeSeconds != null) {
153✔
164
        merged.maxAgeSeconds = (left.maxAgeSeconds ?? 0) > (right.maxAgeSeconds ?? 0) ? left.maxAgeSeconds : right.maxAgeSeconds;
152!
165
      }
166
      if (left.cookieRefresh != null || right.cookieRefresh != null) {
153✔
167
        merged.cookieRefresh = left.cookieRefresh || right.cookieRefresh;
152!
168
      }
169
    }
170
    return merged;
217✔
171
  }
172
  return {
5✔
173
    hook(next, moduleName, disclosure) {
174
      const key = `${disclosure.type}::${disclosure.identifier}`;
217✔
175
      if (!disclosures.hasOwnProperty(key)) {
217✔
176
        disclosures[key] = {
71✔
177
          disclosedBy: [],
178
          ...disclosure
179
        };
180
      }
181
      Object.assign(disclosures[key], mergeDisclosures(disclosures[key], disclosure));
217✔
182
      if (!disclosures[key].disclosedBy.includes(moduleName)) {
217✔
183
        disclosures[key].disclosedBy.push(moduleName);
72✔
184
      }
185
      next(moduleName, disclosure);
217✔
186
    },
187
    getDisclosures() {
188
      return Object.values(disclosures);
4✔
189
    }
190
  }
191
}
192

193
const {hook: discloseStorageHook, getDisclosures: dynamicDisclosures} = dynamicDisclosureCollector();
1✔
194
discloseStorageUse.before(discloseStorageHook);
1✔
195

196
export type StorageDisclosure = Disclosure & {
197
  /**
198
   * URL containing this disclosure, if any.
199
   */
200
  disclosedIn: string | null;
201
  /**
202
   * Names of the modules associated with this disclosure.
203
   */
204
  disclosedBy: string[];
205
}
206

207
function disclosureSummarizer(getDynamicDisclosures = dynamicDisclosures, getSummary = () => getStorageDisclosureSummary(getGlobal().installedModules, metadata.getModuleMetadata)) {
1!
208
  return function() {
1✔
209
    return [].concat(
×
210
      getDynamicDisclosures().map(disclosure => ({
×
211
        disclosedIn: null,
212
        ...(disclosure as any)
213
      })),
214
      getSummary()
215
    );
216
  }
217
}
218

219
const getStorageUseDisclosures: () => StorageDisclosure[] = disclosureSummarizer();
1✔
220

221
declare module '../src/prebidGlobal' {
222
  interface PrebidJS {
223
    getStorageUseDisclosures: typeof getStorageUseDisclosures;
224
  }
225
}
226

227
addApiMethod('getStorageUseDisclosures', getStorageUseDisclosures);
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc