@prodigio-io/sdk 2.0.1 → 2.1.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.
package/dist/index.d.mts CHANGED
@@ -8,9 +8,6 @@ interface ProdigioInitConfig {
8
8
  baseUrl?: string;
9
9
  badge?: {
10
10
  position?: 'bottom-right' | 'bottom-left';
11
- /** Only inject the badge when the current page path starts with one of these strings.
12
- * e.g. ['/careers'] — badge appears on /careers and /careers/job-id but not on / or /about */
13
- paths?: string[];
14
11
  };
15
12
  }
16
13
  interface SalaryRange {
@@ -119,6 +116,7 @@ declare class Prodigio {
119
116
  name: string;
120
117
  content: string;
121
118
  };
119
+ static destroy(): void;
122
120
  static init(config: ProdigioInitConfig): Promise<void>;
123
121
  }
124
122
 
package/dist/index.d.ts CHANGED
@@ -8,9 +8,6 @@ interface ProdigioInitConfig {
8
8
  baseUrl?: string;
9
9
  badge?: {
10
10
  position?: 'bottom-right' | 'bottom-left';
11
- /** Only inject the badge when the current page path starts with one of these strings.
12
- * e.g. ['/careers'] — badge appears on /careers and /careers/job-id but not on / or /about */
13
- paths?: string[];
14
11
  };
15
12
  }
16
13
  interface SalaryRange {
@@ -119,6 +116,7 @@ declare class Prodigio {
119
116
  name: string;
120
117
  content: string;
121
118
  };
119
+ static destroy(): void;
122
120
  static init(config: ProdigioInitConfig): Promise<void>;
123
121
  }
124
122
 
package/dist/index.js CHANGED
@@ -68,7 +68,6 @@ async function verifyBadgeToken(token) {
68
68
  const parts = token.split(".");
69
69
  if (parts.length !== 3) return null;
70
70
  const [headerB64, payloadB64, sigB64] = parts;
71
- const signingInput = `${headerB64}.${payloadB64}`;
72
71
  const signature = base64urlToArrayBuffer(sigB64);
73
72
  const keyData = pemToArrayBuffer(PRODIGIO_PUBLIC_KEY_PEM);
74
73
  const publicKey = await crypto.subtle.importKey(
@@ -78,12 +77,11 @@ async function verifyBadgeToken(token) {
78
77
  false,
79
78
  ["verify"]
80
79
  );
81
- const encoder = new TextEncoder();
82
80
  const valid = await crypto.subtle.verify(
83
81
  "RSASSA-PKCS1-v1_5",
84
82
  publicKey,
85
83
  signature,
86
- encoder.encode(signingInput)
84
+ new TextEncoder().encode(`${headerB64}.${payloadB64}`)
87
85
  );
88
86
  if (!valid) return null;
89
87
  const claims = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
@@ -106,25 +104,13 @@ function injectBadge(position = "bottom-right") {
106
104
  el.setAttribute("data-prodigio-badge", "true");
107
105
  el.setAttribute("aria-label", "Hiring powered by Prodigio");
108
106
  el.style.cssText = `
109
- position: fixed;
110
- bottom: 20px;
111
- ${side}
112
- z-index: 2147483647;
113
- display: inline-flex;
114
- align-items: center;
115
- gap: 6px;
116
- padding: 6px 10px;
117
- background: #ffffff;
118
- border: 1px solid #e5e5e5;
119
- border-radius: 20px;
120
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
107
+ position: fixed; bottom: 20px; ${side}
108
+ z-index: 2147483647; display: inline-flex; align-items: center; gap: 6px;
109
+ padding: 6px 10px; background: #ffffff; border: 1px solid #e5e5e5;
110
+ border-radius: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
121
111
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
122
- font-size: 11px;
123
- font-weight: 500;
124
- color: #666666;
125
- text-decoration: none;
126
- cursor: pointer;
127
- transition: box-shadow 0.15s ease;
112
+ font-size: 11px; font-weight: 500; color: #666666;
113
+ text-decoration: none; cursor: pointer; transition: box-shadow 0.15s ease;
128
114
  `;
129
115
  el.innerHTML = `
130
116
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -149,20 +135,48 @@ function removeBadge() {
149
135
  }
150
136
  function cacheToken(apiKey, token, badgeExemptUntil, configVersion) {
151
137
  try {
152
- const entry = { token, badgeExemptUntil, configVersion, cachedAt: Date.now() };
153
- localStorage.setItem(`${BADGE_CACHE_KEY}_${apiKey}`, JSON.stringify(entry));
138
+ localStorage.setItem(`${BADGE_CACHE_KEY}_${apiKey}`, JSON.stringify({ token, badgeExemptUntil, configVersion, cachedAt: Date.now() }));
154
139
  } catch {
155
140
  }
156
141
  }
157
142
  function getCachedToken(apiKey) {
158
143
  try {
159
144
  const raw = localStorage.getItem(`${BADGE_CACHE_KEY}_${apiKey}`);
160
- if (!raw) return null;
161
- return JSON.parse(raw);
145
+ return raw ? JSON.parse(raw) : null;
162
146
  } catch {
163
147
  return null;
164
148
  }
165
149
  }
150
+ var _routeWatcherInstalled = false;
151
+ var _routeChangeCallbacks = [];
152
+ function installRouteWatcher() {
153
+ if (_routeWatcherInstalled || typeof window === "undefined") return;
154
+ _routeWatcherInstalled = true;
155
+ const dispatch = () => _routeChangeCallbacks.forEach((cb) => cb());
156
+ const orig = {
157
+ push: history.pushState.bind(history),
158
+ replace: history.replaceState.bind(history)
159
+ };
160
+ history.pushState = (...args) => {
161
+ orig.push(...args);
162
+ dispatch();
163
+ };
164
+ history.replaceState = (...args) => {
165
+ orig.replace(...args);
166
+ dispatch();
167
+ };
168
+ window.addEventListener("popstate", dispatch);
169
+ window.addEventListener("hashchange", dispatch);
170
+ }
171
+ var _initialized = false;
172
+ function normalizePath(p) {
173
+ return p.replace(/\/$/, "").split("?")[0];
174
+ }
175
+ function pathMatches(current, careersPath) {
176
+ const c = normalizePath(current);
177
+ const target = normalizePath(careersPath);
178
+ return c === target || c.startsWith(target + "/");
179
+ }
166
180
  var _Prodigio = class _Prodigio {
167
181
  constructor(config) {
168
182
  // ── Jobs ──────────────────────────────────────────────────────────────────
@@ -206,9 +220,7 @@ var _Prodigio = class _Prodigio {
206
220
  const url = new URL(`${this.baseUrl}${path}`);
207
221
  url.searchParams.set("key", this.apiKey);
208
222
  if (params) {
209
- for (const [k, v] of Object.entries(params)) {
210
- if (v !== void 0) url.searchParams.set(k, v);
211
- }
223
+ for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, v);
212
224
  }
213
225
  const res = await fetch(url.toString());
214
226
  if (!res.ok) {
@@ -243,75 +255,75 @@ var _Prodigio = class _Prodigio {
243
255
  }
244
256
  static badge(poweredBy) {
245
257
  const pb = poweredBy != null ? poweredBy : _Prodigio.POWERED_BY;
246
- return {
247
- href: pb.url,
248
- "data-prodigio-badge": "true",
249
- target: "_blank",
250
- rel: "noopener noreferrer",
251
- text: pb.text
252
- };
258
+ return { href: pb.url, "data-prodigio-badge": "true", target: "_blank", rel: "noopener noreferrer", text: pb.text };
253
259
  }
254
260
  static meta() {
255
261
  return { name: "prodigio-badge", content: "true" };
256
262
  }
257
- // ── init() — v2 badge auto-injection ─────────────────────────────────────
263
+ // ── destroy() ─────────────────────────────────────────────────────────────
264
+ static destroy() {
265
+ removeBadge();
266
+ _initialized = false;
267
+ }
268
+ // ── init() ────────────────────────────────────────────────────────────────
258
269
  static async init(config) {
259
270
  var _a;
260
271
  if (typeof window === "undefined") return;
272
+ if (_initialized) return;
273
+ _initialized = true;
261
274
  const { key, badgeToken, baseUrl = DEFAULT_BASE_URL, badge: badgeConfig } = config;
262
275
  const position = (_a = badgeConfig == null ? void 0 : badgeConfig.position) != null ? _a : "bottom-right";
263
- const paths = badgeConfig == null ? void 0 : badgeConfig.paths;
264
- if (paths && paths.length > 0) {
265
- const currentPath = window.location.pathname;
266
- if (!paths.some((p) => currentPath === p || currentPath.startsWith(p + "/"))) {
267
- return;
268
- }
269
- }
270
- const hostname = window.location.hostname;
271
276
  const cached = getCachedToken(key);
272
- let bestToken = null;
273
- let bestTokenRaw = null;
274
277
  const [initClaims, cachedClaims] = await Promise.all([
275
278
  badgeToken ? verifyBadgeToken(badgeToken) : Promise.resolve(null),
276
279
  cached ? verifyBadgeToken(cached.token) : Promise.resolve(null)
277
280
  ]);
281
+ let bestToken = null;
278
282
  if (initClaims && cachedClaims) {
279
- if (new Date(initClaims.badgeExemptUntil) >= new Date(cachedClaims.badgeExemptUntil)) {
280
- bestToken = initClaims;
281
- bestTokenRaw = badgeToken;
282
- } else {
283
- bestToken = cachedClaims;
284
- bestTokenRaw = cached.token;
285
- }
286
- } else if (initClaims) {
287
- bestToken = initClaims;
288
- bestTokenRaw = badgeToken;
289
- } else if (cachedClaims) {
290
- bestToken = cachedClaims;
291
- bestTokenRaw = cached.token;
283
+ bestToken = new Date(initClaims.badgeExemptUntil) >= new Date(cachedClaims.badgeExemptUntil) ? initClaims : cachedClaims;
284
+ } else {
285
+ bestToken = initClaims != null ? initClaims : cachedClaims;
292
286
  }
287
+ const hostname = window.location.hostname;
293
288
  const domainAllowed = bestToken ? bestToken.allowedDomains.length === 0 || bestToken.allowedDomains.includes(hostname) : true;
294
289
  const isExempt = bestToken !== null && !bestToken.badgeRequired && domainAllowed;
295
- if (isExempt) {
296
- removeBadge();
297
- } else if (!bestToken && cached) {
298
- const cacheAge = Date.now() - cached.cachedAt;
299
- if (cacheAge < GRACE_PERIOD_MS) {
290
+ let careersPath = null;
291
+ function evaluateBadge() {
292
+ if (isExempt) {
300
293
  removeBadge();
294
+ return;
295
+ }
296
+ if (careersPath) {
297
+ if (pathMatches(window.location.pathname, careersPath)) {
298
+ injectBadge(position);
299
+ } else {
300
+ removeBadge();
301
+ }
301
302
  } else {
303
+ if (!bestToken && cached) {
304
+ const cacheAge = Date.now() - cached.cachedAt;
305
+ if (cacheAge < GRACE_PERIOD_MS) {
306
+ removeBadge();
307
+ return;
308
+ }
309
+ }
302
310
  injectBadge(position);
303
311
  }
304
- } else {
305
- injectBadge(position);
306
312
  }
313
+ evaluateBadge();
314
+ installRouteWatcher();
315
+ _routeChangeCallbacks.push(evaluateBadge);
307
316
  setTimeout(async () => {
308
317
  var _a2, _b;
309
318
  try {
310
319
  const res = await fetch(`${baseUrl}/api/sdk/config?key=${encodeURIComponent(key)}`);
311
320
  if (!res.ok) return;
312
321
  const data = await res.json();
322
+ if (data.careersPath !== void 0) {
323
+ careersPath = data.careersPath;
324
+ }
313
325
  if (data.badgeRequired) {
314
- injectBadge(position);
326
+ evaluateBadge();
315
327
  return;
316
328
  }
317
329
  if (data.badgeToken) {
@@ -321,12 +333,13 @@ var _Prodigio = class _Prodigio {
321
333
  removeBadge();
322
334
  }
323
335
  }
336
+ evaluateBadge();
324
337
  } catch {
325
338
  }
326
339
  }, 0);
327
340
  }
328
341
  };
329
- // ── Badge (static helpers) ────────────────────────────────────────────────
342
+ // ── Badge static helpers ──────────────────────────────────────────────────
330
343
  _Prodigio.POWERED_BY = {
331
344
  text: "Hiring powered by Prodigio",
332
345
  url: "https://prodigio.io"
package/dist/index.mjs CHANGED
@@ -42,7 +42,6 @@ async function verifyBadgeToken(token) {
42
42
  const parts = token.split(".");
43
43
  if (parts.length !== 3) return null;
44
44
  const [headerB64, payloadB64, sigB64] = parts;
45
- const signingInput = `${headerB64}.${payloadB64}`;
46
45
  const signature = base64urlToArrayBuffer(sigB64);
47
46
  const keyData = pemToArrayBuffer(PRODIGIO_PUBLIC_KEY_PEM);
48
47
  const publicKey = await crypto.subtle.importKey(
@@ -52,12 +51,11 @@ async function verifyBadgeToken(token) {
52
51
  false,
53
52
  ["verify"]
54
53
  );
55
- const encoder = new TextEncoder();
56
54
  const valid = await crypto.subtle.verify(
57
55
  "RSASSA-PKCS1-v1_5",
58
56
  publicKey,
59
57
  signature,
60
- encoder.encode(signingInput)
58
+ new TextEncoder().encode(`${headerB64}.${payloadB64}`)
61
59
  );
62
60
  if (!valid) return null;
63
61
  const claims = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
@@ -80,25 +78,13 @@ function injectBadge(position = "bottom-right") {
80
78
  el.setAttribute("data-prodigio-badge", "true");
81
79
  el.setAttribute("aria-label", "Hiring powered by Prodigio");
82
80
  el.style.cssText = `
83
- position: fixed;
84
- bottom: 20px;
85
- ${side}
86
- z-index: 2147483647;
87
- display: inline-flex;
88
- align-items: center;
89
- gap: 6px;
90
- padding: 6px 10px;
91
- background: #ffffff;
92
- border: 1px solid #e5e5e5;
93
- border-radius: 20px;
94
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
81
+ position: fixed; bottom: 20px; ${side}
82
+ z-index: 2147483647; display: inline-flex; align-items: center; gap: 6px;
83
+ padding: 6px 10px; background: #ffffff; border: 1px solid #e5e5e5;
84
+ border-radius: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
95
85
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
96
- font-size: 11px;
97
- font-weight: 500;
98
- color: #666666;
99
- text-decoration: none;
100
- cursor: pointer;
101
- transition: box-shadow 0.15s ease;
86
+ font-size: 11px; font-weight: 500; color: #666666;
87
+ text-decoration: none; cursor: pointer; transition: box-shadow 0.15s ease;
102
88
  `;
103
89
  el.innerHTML = `
104
90
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -123,20 +109,48 @@ function removeBadge() {
123
109
  }
124
110
  function cacheToken(apiKey, token, badgeExemptUntil, configVersion) {
125
111
  try {
126
- const entry = { token, badgeExemptUntil, configVersion, cachedAt: Date.now() };
127
- localStorage.setItem(`${BADGE_CACHE_KEY}_${apiKey}`, JSON.stringify(entry));
112
+ localStorage.setItem(`${BADGE_CACHE_KEY}_${apiKey}`, JSON.stringify({ token, badgeExemptUntil, configVersion, cachedAt: Date.now() }));
128
113
  } catch {
129
114
  }
130
115
  }
131
116
  function getCachedToken(apiKey) {
132
117
  try {
133
118
  const raw = localStorage.getItem(`${BADGE_CACHE_KEY}_${apiKey}`);
134
- if (!raw) return null;
135
- return JSON.parse(raw);
119
+ return raw ? JSON.parse(raw) : null;
136
120
  } catch {
137
121
  return null;
138
122
  }
139
123
  }
124
+ var _routeWatcherInstalled = false;
125
+ var _routeChangeCallbacks = [];
126
+ function installRouteWatcher() {
127
+ if (_routeWatcherInstalled || typeof window === "undefined") return;
128
+ _routeWatcherInstalled = true;
129
+ const dispatch = () => _routeChangeCallbacks.forEach((cb) => cb());
130
+ const orig = {
131
+ push: history.pushState.bind(history),
132
+ replace: history.replaceState.bind(history)
133
+ };
134
+ history.pushState = (...args) => {
135
+ orig.push(...args);
136
+ dispatch();
137
+ };
138
+ history.replaceState = (...args) => {
139
+ orig.replace(...args);
140
+ dispatch();
141
+ };
142
+ window.addEventListener("popstate", dispatch);
143
+ window.addEventListener("hashchange", dispatch);
144
+ }
145
+ var _initialized = false;
146
+ function normalizePath(p) {
147
+ return p.replace(/\/$/, "").split("?")[0];
148
+ }
149
+ function pathMatches(current, careersPath) {
150
+ const c = normalizePath(current);
151
+ const target = normalizePath(careersPath);
152
+ return c === target || c.startsWith(target + "/");
153
+ }
140
154
  var _Prodigio = class _Prodigio {
141
155
  constructor(config) {
142
156
  // ── Jobs ──────────────────────────────────────────────────────────────────
@@ -180,9 +194,7 @@ var _Prodigio = class _Prodigio {
180
194
  const url = new URL(`${this.baseUrl}${path}`);
181
195
  url.searchParams.set("key", this.apiKey);
182
196
  if (params) {
183
- for (const [k, v] of Object.entries(params)) {
184
- if (v !== void 0) url.searchParams.set(k, v);
185
- }
197
+ for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, v);
186
198
  }
187
199
  const res = await fetch(url.toString());
188
200
  if (!res.ok) {
@@ -217,75 +229,75 @@ var _Prodigio = class _Prodigio {
217
229
  }
218
230
  static badge(poweredBy) {
219
231
  const pb = poweredBy != null ? poweredBy : _Prodigio.POWERED_BY;
220
- return {
221
- href: pb.url,
222
- "data-prodigio-badge": "true",
223
- target: "_blank",
224
- rel: "noopener noreferrer",
225
- text: pb.text
226
- };
232
+ return { href: pb.url, "data-prodigio-badge": "true", target: "_blank", rel: "noopener noreferrer", text: pb.text };
227
233
  }
228
234
  static meta() {
229
235
  return { name: "prodigio-badge", content: "true" };
230
236
  }
231
- // ── init() — v2 badge auto-injection ─────────────────────────────────────
237
+ // ── destroy() ─────────────────────────────────────────────────────────────
238
+ static destroy() {
239
+ removeBadge();
240
+ _initialized = false;
241
+ }
242
+ // ── init() ────────────────────────────────────────────────────────────────
232
243
  static async init(config) {
233
244
  var _a;
234
245
  if (typeof window === "undefined") return;
246
+ if (_initialized) return;
247
+ _initialized = true;
235
248
  const { key, badgeToken, baseUrl = DEFAULT_BASE_URL, badge: badgeConfig } = config;
236
249
  const position = (_a = badgeConfig == null ? void 0 : badgeConfig.position) != null ? _a : "bottom-right";
237
- const paths = badgeConfig == null ? void 0 : badgeConfig.paths;
238
- if (paths && paths.length > 0) {
239
- const currentPath = window.location.pathname;
240
- if (!paths.some((p) => currentPath === p || currentPath.startsWith(p + "/"))) {
241
- return;
242
- }
243
- }
244
- const hostname = window.location.hostname;
245
250
  const cached = getCachedToken(key);
246
- let bestToken = null;
247
- let bestTokenRaw = null;
248
251
  const [initClaims, cachedClaims] = await Promise.all([
249
252
  badgeToken ? verifyBadgeToken(badgeToken) : Promise.resolve(null),
250
253
  cached ? verifyBadgeToken(cached.token) : Promise.resolve(null)
251
254
  ]);
255
+ let bestToken = null;
252
256
  if (initClaims && cachedClaims) {
253
- if (new Date(initClaims.badgeExemptUntil) >= new Date(cachedClaims.badgeExemptUntil)) {
254
- bestToken = initClaims;
255
- bestTokenRaw = badgeToken;
256
- } else {
257
- bestToken = cachedClaims;
258
- bestTokenRaw = cached.token;
259
- }
260
- } else if (initClaims) {
261
- bestToken = initClaims;
262
- bestTokenRaw = badgeToken;
263
- } else if (cachedClaims) {
264
- bestToken = cachedClaims;
265
- bestTokenRaw = cached.token;
257
+ bestToken = new Date(initClaims.badgeExemptUntil) >= new Date(cachedClaims.badgeExemptUntil) ? initClaims : cachedClaims;
258
+ } else {
259
+ bestToken = initClaims != null ? initClaims : cachedClaims;
266
260
  }
261
+ const hostname = window.location.hostname;
267
262
  const domainAllowed = bestToken ? bestToken.allowedDomains.length === 0 || bestToken.allowedDomains.includes(hostname) : true;
268
263
  const isExempt = bestToken !== null && !bestToken.badgeRequired && domainAllowed;
269
- if (isExempt) {
270
- removeBadge();
271
- } else if (!bestToken && cached) {
272
- const cacheAge = Date.now() - cached.cachedAt;
273
- if (cacheAge < GRACE_PERIOD_MS) {
264
+ let careersPath = null;
265
+ function evaluateBadge() {
266
+ if (isExempt) {
274
267
  removeBadge();
268
+ return;
269
+ }
270
+ if (careersPath) {
271
+ if (pathMatches(window.location.pathname, careersPath)) {
272
+ injectBadge(position);
273
+ } else {
274
+ removeBadge();
275
+ }
275
276
  } else {
277
+ if (!bestToken && cached) {
278
+ const cacheAge = Date.now() - cached.cachedAt;
279
+ if (cacheAge < GRACE_PERIOD_MS) {
280
+ removeBadge();
281
+ return;
282
+ }
283
+ }
276
284
  injectBadge(position);
277
285
  }
278
- } else {
279
- injectBadge(position);
280
286
  }
287
+ evaluateBadge();
288
+ installRouteWatcher();
289
+ _routeChangeCallbacks.push(evaluateBadge);
281
290
  setTimeout(async () => {
282
291
  var _a2, _b;
283
292
  try {
284
293
  const res = await fetch(`${baseUrl}/api/sdk/config?key=${encodeURIComponent(key)}`);
285
294
  if (!res.ok) return;
286
295
  const data = await res.json();
296
+ if (data.careersPath !== void 0) {
297
+ careersPath = data.careersPath;
298
+ }
287
299
  if (data.badgeRequired) {
288
- injectBadge(position);
300
+ evaluateBadge();
289
301
  return;
290
302
  }
291
303
  if (data.badgeToken) {
@@ -295,12 +307,13 @@ var _Prodigio = class _Prodigio {
295
307
  removeBadge();
296
308
  }
297
309
  }
310
+ evaluateBadge();
298
311
  } catch {
299
312
  }
300
313
  }, 0);
301
314
  }
302
315
  };
303
- // ── Badge (static helpers) ────────────────────────────────────────────────
316
+ // ── Badge static helpers ──────────────────────────────────────────────────
304
317
  _Prodigio.POWERED_BY = {
305
318
  text: "Hiring powered by Prodigio",
306
319
  url: "https://prodigio.io"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prodigio-io/sdk",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Official JavaScript SDK for the Prodigio hiring API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",