@hiofu/apply-sdk 0.1.4 → 0.1.6-beta.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/README.md CHANGED
@@ -18,50 +18,51 @@ npm install @hiofu/apply-sdk
18
18
 
19
19
  ## Quick Start
20
20
 
21
- Use sandbox keys for testing and live keys only after HIOFU grants
22
- production access.
21
+ ### React (simplest)
22
+
23
+ ```tsx
24
+ import { HiofuApplyButton } from "@hiofu/apply-sdk/react";
25
+
26
+ export function ApplyButton({ jobId, jobTitle }) {
27
+ return (
28
+ <HiofuApplyButton
29
+ clientId={process.env.NEXT_PUBLIC_HIOFU_KEY}
30
+ variant="primary"
31
+ options={{ jobId, jobTitle }}
32
+ />
33
+ );
34
+ }
35
+ ```
36
+
37
+ The SDK infers sandbox/production from the key prefix and uses the
38
+ HIOFU-hosted callback by default. No extra config needed.
39
+
40
+ ### Vanilla JS
23
41
 
24
42
  ```ts
25
43
  import { createApplyClient } from "@hiofu/apply-sdk";
26
44
 
27
45
  const hiofu = createApplyClient({
28
- environment: "sandbox",
29
46
  publicKey: "pk_test_xxx",
30
- redirectUri: "https://jobs.example.com/oauth/callback.html",
31
- onComplete(result) {
32
- console.log(result.application.id, result.application.status);
33
- },
34
- onError(error) {
35
- console.error(error.code, error.correlationId, error.message);
36
- },
37
47
  });
38
48
 
39
49
  const result = await hiofu.apply({
40
50
  jobId: "job_123",
41
- jobTitle: "Senior Engineer",
42
- employerId: "emp_456",
43
- employerName: "Acme Inc",
44
- role: {
45
- externalRoleId: "job_123",
46
- externalEmployerId: "emp_456",
47
- title: "Senior Engineer",
48
- locations: ["London", "Remote"],
49
- metadata: { department: "Engineering" },
50
- },
51
+ role: { title: "Senior Engineer" },
51
52
  idempotencyKey: "apply_attempt_01JXYZ...",
52
53
  });
53
54
 
54
- console.log(result.application.id);
55
- console.log(result.snapshot?.trustBand);
55
+ console.log(result.applicationId);
56
56
  ```
57
57
 
58
58
  ## Configuration
59
59
 
60
- `createApplyClient` is the recommended API. It resolves the correct HIOFU URLs from `environment` and rejects key/environment mismatches before opening the popup.
60
+ `createApplyClient` resolves the correct HIOFU URLs from the key prefix and
61
+ rejects key/environment mismatches before opening the popup.
61
62
 
62
- - `environment` (required): `sandbox` or `production`.
63
63
  - `publicKey` (required): Your publishable key, e.g. `pk_test_…` or `pk_live_…`.
64
- - `redirectUri` (required): One of your registered callback URLs.
64
+ - `environment` (optional): Inferred from key prefix. Override with `"sandbox"` or `"production"` if needed.
65
+ - `redirectUri` (optional): Defaults to the HIOFU-hosted callback. Set this only if you host your own callback page.
65
66
  - `storage` (default `memory`): Token storage, either `memory` (cleared on reload) or `session` (persists the access token for the browser session; refresh tokens are not persisted in browser storage).
66
67
  - `scopes` / `applyScopes` (optional): Default scopes for `authorize()` / `apply()`.
67
68
  - `popupOptions.timeoutMs` (default 300_000): Popup wait limit in milliseconds.
@@ -72,13 +73,15 @@ console.log(result.snapshot?.trustBand);
72
73
 
73
74
  `HiofuClient` remains available as the lower-level compatibility API using `clientId`, `hiofuOrigin`, and `apiBase`.
74
75
 
75
- ## Manage Setup From The SDK
76
+ ## Optional Backend Setup Automation
76
77
 
77
78
  If your team prefers automation over the HIOFU Developer settings UI,
78
79
  the same setup actions are available from the management client.
79
80
 
80
- Use this from your backend or internal tooling. The access token should
81
- stay on trusted infrastructure.
81
+ The public browser Apply flow does not need an employer access token.
82
+ Use this management client only from your backend or internal tooling
83
+ when you want to automate setup. The access token should stay on trusted
84
+ infrastructure and must never be exposed to browser code.
82
85
 
83
86
  ```ts
84
87
  import { createManagementClient } from "@hiofu/apply-sdk";
@@ -102,16 +105,11 @@ const role = await management.createRole({
102
105
  await management.updateRoleStatus(role.id, "hiring");
103
106
  ```
104
107
 
105
- ### Issue the publishable key, register the callback, and save the route
108
+ ### Issue the publishable key and save the route
106
109
 
107
110
  ```ts
108
111
  await management.issuePublishableKey();
109
112
 
110
- await management.addRedirectUri({
111
- mode: "test",
112
- uri: "https://jobs.example.com/oauth/callback-shim.html",
113
- });
114
-
115
113
  await management.saveRoleMapping({
116
114
  mode: "test",
117
115
  externalRoleId: "job_123",
@@ -119,6 +117,16 @@ await management.saveRoleMapping({
119
117
  });
120
118
  ```
121
119
 
120
+ Register a callback URI only if you override the SDK's HIOFU-hosted callback
121
+ with your own `redirectUri`.
122
+
123
+ ```ts
124
+ await management.addRedirectUri({
125
+ mode: "test",
126
+ uri: "https://jobs.example.com/oauth/callback.html",
127
+ });
128
+ ```
129
+
122
130
  ### Update or remove setup later
123
131
 
124
132
  ```ts
@@ -152,62 +160,41 @@ HIOFU Developer settings UI:
152
160
 
153
161
  ### Sandbox vs Production
154
162
 
155
- Switch environments by key and base URLs. The SDK rejects mismatches before
156
- opening the popup: `pk_test_*` must use sandbox URLs, and `pk_live_*` must use
157
- production URLs.
163
+ Switch environments by key. The SDK infers the correct URLs from the prefix.
158
164
 
159
165
  ```ts
160
- const isSandbox = process.env.NODE_ENV !== "production"; // or your own flag
166
+ const isSandbox = process.env.NODE_ENV !== "production";
161
167
 
162
168
  const hiofu = createApplyClient({
163
- environment: isSandbox ? "sandbox" : "production",
164
169
  publicKey: isSandbox ? "pk_test_xxx" : "pk_live_xxx",
165
- redirectUri: `${window.location.origin}/oauth/callback.html`,
166
170
  });
167
171
  ```
168
172
 
169
173
  ## React
170
174
 
175
+ ### Standalone button (simplest)
176
+
171
177
  ```tsx
172
- import { HiofuApplyButton, HiofuProvider } from "@hiofu/apply-sdk/react";
178
+ import { HiofuApplyButton } from "@hiofu/apply-sdk/react";
173
179
 
174
- export default function App() {
175
- return (
176
- <HiofuProvider
177
- config={{
178
- clientId: "pk_test_xxx",
179
- hiofuOrigin: "https://sandbox.hiofu.com",
180
- apiBase: "https://api.sandbox.hiofu.com/api",
181
- redirectUri: "https://jobs.example.com/oauth/callback.html",
182
- // Optional extras with sensible defaults
183
- // storage: "session",
184
- // applyScopes: ["applications.write", "passport.snapshot"],
185
- // onEvent: (e) => console.debug("hiofu", e),
186
- }}
187
- >
188
- <ApplyButton />
189
- </HiofuProvider>
190
- );
191
- }
180
+ <HiofuApplyButton
181
+ clientId="pk_test_xxx"
182
+ variant="primary"
183
+ options={{ jobId: "job_123", jobTitle: "Senior Engineer" }}
184
+ />
185
+ ```
192
186
 
193
- function ApplyButton() {
194
- return (
195
- <HiofuApplyButton
196
- variant="primary"
197
- options={{
198
- jobId: "job_123",
199
- jobTitle: "Senior Engineer",
200
- employerId: "emp_456",
201
- employerName: "Acme Inc",
202
- role: {
203
- externalRoleId: "job_123",
204
- externalEmployerId: "emp_456",
205
- title: "Senior Engineer",
206
- },
207
- }}
208
- />
209
- );
210
- }
187
+ No provider needed for a single button. The SDK infers everything from the key.
188
+
189
+ ### With provider (multiple buttons sharing config)
190
+
191
+ ```tsx
192
+ import { HiofuApplyButton, HiofuProvider } from "@hiofu/apply-sdk/react";
193
+
194
+ <HiofuProvider config={{ clientId: "pk_test_xxx" }}>
195
+ <HiofuApplyButton options={{ jobId: "job_1", jobTitle: "Role A" }} />
196
+ <HiofuApplyButton options={{ jobId: "job_2", jobTitle: "Role B" }} />
197
+ </HiofuProvider>
211
198
  ```
212
199
 
213
200
  `HiofuApplyButton` ships three variants: `primary`, `secondary`, and `ghost`.
@@ -257,19 +244,13 @@ function ApplyButton() {
257
244
  <script
258
245
  src="https://cdn.hiofu.com/apply-sdk.global.js"
259
246
  data-client-id="pk_test_xxx"
260
- data-hiofu-origin="https://sandbox.hiofu.com"
261
- data-api-base="https://api.sandbox.hiofu.com/api"
262
- data-redirect-uri="https://jobs.example.com/oauth/callback.html"
263
247
  ></script>
264
248
 
265
249
  <button
266
250
  data-hiofu-apply
267
251
  data-job-id="job_123"
268
252
  data-job-title="Senior Engineer"
269
- data-employer-id="emp_456"
270
253
  data-employer-name="Acme Inc"
271
- data-external-role-id="job_123"
272
- data-external-employer-id="emp_456"
273
254
  data-role-department="Engineering"
274
255
  data-role-locations="London,Remote"
275
256
  >
@@ -321,9 +302,15 @@ await hiofu.apply({
321
302
  });
322
303
  ```
323
304
 
324
- ## Callback Handling
305
+ ## Custom Callback Handling
306
+
307
+ Most integrations do not need a callback page. When `redirectUri` is omitted,
308
+ the SDK uses the HIOFU-hosted callback for the key's environment.
325
309
 
326
- Register your callback page (e.g. `https://jobs.example.com/oauth/callback.html`) in HIOFU Developer settings. Do not register callback URLs on domains you do not control. The page should relay the OAuth result back to the opener. Minimal example:
310
+ Only host your own callback when you have a specific same-origin requirement.
311
+ In that case, register the exact callback URL in HIOFU Developer settings and
312
+ pass it as `redirectUri`. Do not register callback URLs on domains you do not
313
+ control. The page should relay the OAuth result back to the opener:
327
314
 
328
315
  ```html
329
316
  <!doctype html>
@@ -395,7 +382,7 @@ Returned payload highlights:
395
382
 
396
383
  ## Security Notes
397
384
 
398
- - Register your callback page (e.g. `https://jobs.example.com/oauth/callback.html`) and pass it explicitly as `redirectUri`; never use domains you do not control.
385
+ - Prefer the HIOFU-hosted callback. If you host your own callback page, register only domains you control and pass the URL explicitly as `redirectUri`.
399
386
  - Browser integrations receive a short-lived access token only through the SDK runtime. Refresh tokens are never exposed to your UI code.
400
387
  - The SDK does not inject third-party fonts or global styles into your pages. Keep typography ownership inside your app shell.
401
388
  - Do not log raw application payloads in production UIs.
@@ -452,7 +439,6 @@ import {
452
439
 
453
440
  const hiofu = new HiofuClient({
454
441
  clientId: "pk_live_xxx",
455
- redirectUri: `${location.origin}/oauth/callback.html`,
456
442
  });
457
443
 
458
444
  try {
@@ -338,9 +338,6 @@ var HiofuPopupError = class extends Error {
338
338
  function resolveRedirectUri(config, hiofuOrigin) {
339
339
  if (config.redirectUri) return config.redirectUri;
340
340
  const hiofuBaseOrigin = new URL(hiofuOrigin).origin;
341
- if (typeof window !== "undefined" && window.location.origin && window.location.origin !== hiofuBaseOrigin) {
342
- return new URL("/oauth/callback.html", window.location.origin).toString();
343
- }
344
341
  return `${hiofuBaseOrigin}/oauth/callback-shim`;
345
342
  }
346
343
  async function authorize(config, scopes, context, callbacks) {
@@ -354,6 +351,9 @@ async function authorize(config, scopes, context, callbacks) {
354
351
  const url = new URL(`${hiofuOrigin}/oauth/consent`);
355
352
  url.searchParams.set("client_id", config.clientId);
356
353
  url.searchParams.set("redirect_uri", redirectUri);
354
+ if (window.location?.origin) {
355
+ url.searchParams.set("opener_origin", window.location.origin);
356
+ }
357
357
  url.searchParams.set("scope", scopes.join(" "));
358
358
  url.searchParams.set("state", state);
359
359
  url.searchParams.set("code_challenge", challenge);
@@ -389,6 +389,7 @@ async function authorize(config, scopes, context, callbacks) {
389
389
  let channel = null;
390
390
  let openerLostAttention = false;
391
391
  let openerRegainedAttention = false;
392
+ let popupClosedAt = null;
392
393
  const cleanup = () => {
393
394
  settled = true;
394
395
  window.removeEventListener("message", onMessage);
@@ -538,14 +539,28 @@ async function authorize(config, scopes, context, callbacks) {
538
539
  }
539
540
  } catch {
540
541
  }
541
- if (popup.closed && openerRegainedAttention) {
542
+ if (popup.closed) {
543
+ popupClosedAt ?? (popupClosedAt = Date.now());
544
+ } else {
545
+ popupClosedAt = null;
546
+ }
547
+ const canTreatAsClosed = popup.closed && popupClosedAt !== null && openerRegainedAttention && document.hasFocus() && Date.now() - popupClosedAt > 5e3;
548
+ if (canTreatAsClosed) {
542
549
  try {
543
550
  const raw = localStorage.getItem("hiofu_oauth_result");
544
- if (raw) {
545
- handleResult(JSON.parse(raw), "close_check");
546
- }
551
+ if (raw) handleResult(JSON.parse(raw), "close_check");
547
552
  } catch {
548
553
  }
554
+ if (!settled) {
555
+ cleanup();
556
+ callbacks?.onPopupClosed?.("user_closed");
557
+ reject(
558
+ new HiofuPopupError(
559
+ "user_closed",
560
+ "Authorization popup closed before HIOFU could complete the request. Please try again."
561
+ )
562
+ );
563
+ }
549
564
  }
550
565
  }, 500);
551
566
  overallTimeout = window.setTimeout(() => {
@@ -699,10 +714,13 @@ var HiofuClient = class {
699
714
  if (!config.clientId) {
700
715
  throw new Error("HiofuClient: clientId is required");
701
716
  }
717
+ const isSandboxKey = config.clientId.startsWith("pk_test_");
718
+ const defaultOrigin = isSandboxKey ? "https://sandbox.hiofu.com" : "https://hiofu.com";
719
+ const defaultApiBase = isSandboxKey ? "https://api.sandbox.hiofu.com/api" : "https://api.hiofu.com/api";
702
720
  this.config = {
703
721
  ...config,
704
- hiofuOrigin: config.hiofuOrigin ?? "https://hiofu.com",
705
- apiBase: config.apiBase ?? "https://api.hiofu.com/api",
722
+ hiofuOrigin: config.hiofuOrigin ?? defaultOrigin,
723
+ apiBase: config.apiBase ?? defaultApiBase,
706
724
  storage: config.storage ?? "memory",
707
725
  authorizeTimeoutMs: config.authorizeTimeoutMs ?? 5 * 6e4
708
726
  };
@@ -824,12 +842,21 @@ var HiofuClient = class {
824
842
  idempotencyKey,
825
843
  variationId: selectedVariationId
826
844
  });
845
+ const resolvedRole = opts.role ? {
846
+ ...opts.role,
847
+ externalRoleId: opts.role.externalRoleId || opts.jobId,
848
+ title: opts.role.title || opts.jobTitle,
849
+ externalEmployerId: opts.role.externalEmployerId || opts.employerId || void 0
850
+ } : {
851
+ externalRoleId: opts.jobId,
852
+ title: opts.jobTitle
853
+ };
827
854
  const json = await submitApplication(this.config, access, {
828
855
  jobId: opts.jobId,
829
856
  jobTitle: opts.jobTitle,
830
857
  employerId: opts.employerId,
831
858
  employerName: opts.employerName,
832
- role: opts.role,
859
+ role: resolvedRole,
833
860
  variationId: selectedVariationId ?? void 0,
834
861
  idempotencyKey
835
862
  });
@@ -11,8 +11,8 @@ interface HiofuConfig {
11
11
  /** Optional default scopes for `apply()`. */
12
12
  applyScopes?: HiofuScope[];
13
13
  /** Where the popup will redirect on success. Must be one of the partner's
14
- * registered URIs. If omitted in the browser, the SDK falls back to a
15
- * same-origin `/oauth/callback.html` page when possible. */
14
+ * registered URIs. If omitted, the SDK uses the HIOFU-hosted callback for
15
+ * the selected environment. */
16
16
  redirectUri?: string;
17
17
  /** Where to store the access token client-side. */
18
18
  storage?: "session" | "memory";
@@ -26,11 +26,12 @@ interface HiofuTokenSet {
26
26
  scopes: HiofuScope[];
27
27
  }
28
28
  interface HiofuApplyRole {
29
- /** Partner-owned stable role/job identifier. */
30
- externalRoleId: string;
29
+ /** Partner-owned stable role/job identifier. Defaults to jobId when omitted. */
30
+ externalRoleId?: string;
31
31
  /** Partner-owned stable employer/company identifier, optional for direct HIOFU-domain integrations. */
32
32
  externalEmployerId?: string;
33
- title: string;
33
+ /** Defaults to jobTitle when omitted. */
34
+ title?: string;
34
35
  description?: string;
35
36
  department?: string;
36
37
  locations?: string[];
@@ -424,4 +425,4 @@ declare class HiofuClient {
424
425
  logout(): Promise<void>;
425
426
  }
426
427
 
427
- export { DEFAULT_APPLY_SCOPES as D, HiofuClient as H, type HiofuScope as a, type HiofuApplyResult as b, type HiofuConfig as c, type HiofuEvent as d, type HiofuManagementConfig as e, type HiofuManagementListRolesParams as f, type HiofuManagementPaginatedResult as g, type HiofuManagementRoleListItem as h, type HiofuManagementRole as i, type HiofuManagementCreateRoleInput as j, type HiofuManagementUpdateRoleInput as k, type HiofuManagementRoleStatus as l, type HiofuManagementDeveloperSettings as m, type HiofuManagementSaveRoleMappingInput as n, type HiofuManagementAddRedirectUriInput as o, type HiofuApplyOptions as p, type HiofuTokenSet as q, DEFAULT_AUTHORIZE_SCOPES as r, type HiofuApplyRole as s, type HiofuManagementEnvironment as t, type HiofuManagementEnvironmentMode as u, type HiofuManagementRedirectUri as v, type HiofuManagementRoleDimension as w, type HiofuManagementRoleDimensionSkill as x, type HiofuManagementRoleMapping as y, type HiofuPartner as z };
428
+ export { DEFAULT_APPLY_SCOPES as D, type HiofuApplyOptions as H, DEFAULT_AUTHORIZE_SCOPES as a, type HiofuApplyResult as b, type HiofuApplyRole as c, HiofuClient as d, type HiofuConfig as e, type HiofuEvent as f, type HiofuManagementAddRedirectUriInput as g, type HiofuManagementConfig as h, type HiofuManagementCreateRoleInput as i, type HiofuManagementDeveloperSettings as j, type HiofuManagementEnvironment as k, type HiofuManagementEnvironmentMode as l, type HiofuManagementListRolesParams as m, type HiofuManagementPaginatedResult as n, type HiofuManagementRedirectUri as o, type HiofuManagementRole as p, type HiofuManagementRoleDimension as q, type HiofuManagementRoleDimensionSkill as r, type HiofuManagementRoleListItem as s, type HiofuManagementRoleMapping as t, type HiofuManagementRoleStatus as u, type HiofuManagementSaveRoleMappingInput as v, type HiofuManagementUpdateRoleInput as w, type HiofuPartner as x, type HiofuScope as y, type HiofuTokenSet as z };
@@ -11,8 +11,8 @@ interface HiofuConfig {
11
11
  /** Optional default scopes for `apply()`. */
12
12
  applyScopes?: HiofuScope[];
13
13
  /** Where the popup will redirect on success. Must be one of the partner's
14
- * registered URIs. If omitted in the browser, the SDK falls back to a
15
- * same-origin `/oauth/callback.html` page when possible. */
14
+ * registered URIs. If omitted, the SDK uses the HIOFU-hosted callback for
15
+ * the selected environment. */
16
16
  redirectUri?: string;
17
17
  /** Where to store the access token client-side. */
18
18
  storage?: "session" | "memory";
@@ -26,11 +26,12 @@ interface HiofuTokenSet {
26
26
  scopes: HiofuScope[];
27
27
  }
28
28
  interface HiofuApplyRole {
29
- /** Partner-owned stable role/job identifier. */
30
- externalRoleId: string;
29
+ /** Partner-owned stable role/job identifier. Defaults to jobId when omitted. */
30
+ externalRoleId?: string;
31
31
  /** Partner-owned stable employer/company identifier, optional for direct HIOFU-domain integrations. */
32
32
  externalEmployerId?: string;
33
- title: string;
33
+ /** Defaults to jobTitle when omitted. */
34
+ title?: string;
34
35
  description?: string;
35
36
  department?: string;
36
37
  locations?: string[];
@@ -424,4 +425,4 @@ declare class HiofuClient {
424
425
  logout(): Promise<void>;
425
426
  }
426
427
 
427
- export { DEFAULT_APPLY_SCOPES as D, HiofuClient as H, type HiofuScope as a, type HiofuApplyResult as b, type HiofuConfig as c, type HiofuEvent as d, type HiofuManagementConfig as e, type HiofuManagementListRolesParams as f, type HiofuManagementPaginatedResult as g, type HiofuManagementRoleListItem as h, type HiofuManagementRole as i, type HiofuManagementCreateRoleInput as j, type HiofuManagementUpdateRoleInput as k, type HiofuManagementRoleStatus as l, type HiofuManagementDeveloperSettings as m, type HiofuManagementSaveRoleMappingInput as n, type HiofuManagementAddRedirectUriInput as o, type HiofuApplyOptions as p, type HiofuTokenSet as q, DEFAULT_AUTHORIZE_SCOPES as r, type HiofuApplyRole as s, type HiofuManagementEnvironment as t, type HiofuManagementEnvironmentMode as u, type HiofuManagementRedirectUri as v, type HiofuManagementRoleDimension as w, type HiofuManagementRoleDimensionSkill as x, type HiofuManagementRoleMapping as y, type HiofuPartner as z };
428
+ export { DEFAULT_APPLY_SCOPES as D, type HiofuApplyOptions as H, DEFAULT_AUTHORIZE_SCOPES as a, type HiofuApplyResult as b, type HiofuApplyRole as c, HiofuClient as d, type HiofuConfig as e, type HiofuEvent as f, type HiofuManagementAddRedirectUriInput as g, type HiofuManagementConfig as h, type HiofuManagementCreateRoleInput as i, type HiofuManagementDeveloperSettings as j, type HiofuManagementEnvironment as k, type HiofuManagementEnvironmentMode as l, type HiofuManagementListRolesParams as m, type HiofuManagementPaginatedResult as n, type HiofuManagementRedirectUri as o, type HiofuManagementRole as p, type HiofuManagementRoleDimension as q, type HiofuManagementRoleDimensionSkill as r, type HiofuManagementRoleListItem as s, type HiofuManagementRoleMapping as t, type HiofuManagementRoleStatus as u, type HiofuManagementSaveRoleMappingInput as v, type HiofuManagementUpdateRoleInput as w, type HiofuPartner as x, type HiofuScope as y, type HiofuTokenSet as z };
package/dist/index.cjs CHANGED
@@ -385,9 +385,6 @@ var HiofuPopupError = class extends Error {
385
385
  function resolveRedirectUri(config, hiofuOrigin) {
386
386
  if (config.redirectUri) return config.redirectUri;
387
387
  const hiofuBaseOrigin = new URL(hiofuOrigin).origin;
388
- if (typeof window !== "undefined" && window.location.origin && window.location.origin !== hiofuBaseOrigin) {
389
- return new URL("/oauth/callback.html", window.location.origin).toString();
390
- }
391
388
  return `${hiofuBaseOrigin}/oauth/callback-shim`;
392
389
  }
393
390
  async function authorize(config, scopes, context, callbacks) {
@@ -401,6 +398,9 @@ async function authorize(config, scopes, context, callbacks) {
401
398
  const url = new URL(`${hiofuOrigin}/oauth/consent`);
402
399
  url.searchParams.set("client_id", config.clientId);
403
400
  url.searchParams.set("redirect_uri", redirectUri);
401
+ if (window.location?.origin) {
402
+ url.searchParams.set("opener_origin", window.location.origin);
403
+ }
404
404
  url.searchParams.set("scope", scopes.join(" "));
405
405
  url.searchParams.set("state", state);
406
406
  url.searchParams.set("code_challenge", challenge);
@@ -436,6 +436,7 @@ async function authorize(config, scopes, context, callbacks) {
436
436
  let channel = null;
437
437
  let openerLostAttention = false;
438
438
  let openerRegainedAttention = false;
439
+ let popupClosedAt = null;
439
440
  const cleanup = () => {
440
441
  settled = true;
441
442
  window.removeEventListener("message", onMessage);
@@ -585,14 +586,28 @@ async function authorize(config, scopes, context, callbacks) {
585
586
  }
586
587
  } catch {
587
588
  }
588
- if (popup.closed && openerRegainedAttention) {
589
+ if (popup.closed) {
590
+ popupClosedAt ?? (popupClosedAt = Date.now());
591
+ } else {
592
+ popupClosedAt = null;
593
+ }
594
+ const canTreatAsClosed = popup.closed && popupClosedAt !== null && openerRegainedAttention && document.hasFocus() && Date.now() - popupClosedAt > 5e3;
595
+ if (canTreatAsClosed) {
589
596
  try {
590
597
  const raw = localStorage.getItem("hiofu_oauth_result");
591
- if (raw) {
592
- handleResult(JSON.parse(raw), "close_check");
593
- }
598
+ if (raw) handleResult(JSON.parse(raw), "close_check");
594
599
  } catch {
595
600
  }
601
+ if (!settled) {
602
+ cleanup();
603
+ callbacks?.onPopupClosed?.("user_closed");
604
+ reject(
605
+ new HiofuPopupError(
606
+ "user_closed",
607
+ "Authorization popup closed before HIOFU could complete the request. Please try again."
608
+ )
609
+ );
610
+ }
596
611
  }
597
612
  }, 500);
598
613
  overallTimeout = window.setTimeout(() => {
@@ -746,10 +761,13 @@ var HiofuClient = class {
746
761
  if (!config.clientId) {
747
762
  throw new Error("HiofuClient: clientId is required");
748
763
  }
764
+ const isSandboxKey = config.clientId.startsWith("pk_test_");
765
+ const defaultOrigin = isSandboxKey ? "https://sandbox.hiofu.com" : "https://hiofu.com";
766
+ const defaultApiBase = isSandboxKey ? "https://api.sandbox.hiofu.com/api" : "https://api.hiofu.com/api";
749
767
  this.config = {
750
768
  ...config,
751
- hiofuOrigin: config.hiofuOrigin ?? "https://hiofu.com",
752
- apiBase: config.apiBase ?? "https://api.hiofu.com/api",
769
+ hiofuOrigin: config.hiofuOrigin ?? defaultOrigin,
770
+ apiBase: config.apiBase ?? defaultApiBase,
753
771
  storage: config.storage ?? "memory",
754
772
  authorizeTimeoutMs: config.authorizeTimeoutMs ?? 5 * 6e4
755
773
  };
@@ -871,12 +889,21 @@ var HiofuClient = class {
871
889
  idempotencyKey,
872
890
  variationId: selectedVariationId
873
891
  });
892
+ const resolvedRole = opts.role ? {
893
+ ...opts.role,
894
+ externalRoleId: opts.role.externalRoleId || opts.jobId,
895
+ title: opts.role.title || opts.jobTitle,
896
+ externalEmployerId: opts.role.externalEmployerId || opts.employerId || void 0
897
+ } : {
898
+ externalRoleId: opts.jobId,
899
+ title: opts.jobTitle
900
+ };
874
901
  const json = await submitApplication(this.config, access, {
875
902
  jobId: opts.jobId,
876
903
  jobTitle: opts.jobTitle,
877
904
  employerId: opts.employerId,
878
905
  employerName: opts.employerName,
879
- role: opts.role,
906
+ role: resolvedRole,
880
907
  variationId: selectedVariationId ?? void 0,
881
908
  idempotencyKey
882
909
  });
@@ -1078,6 +1105,11 @@ var HiofuApplyError = class extends Error {
1078
1105
  this.cause = args.cause;
1079
1106
  }
1080
1107
  };
1108
+ function inferEnvironment(publicKey) {
1109
+ if (publicKey.startsWith("pk_test_")) return "sandbox";
1110
+ if (publicKey.startsWith("pk_live_")) return "production";
1111
+ return null;
1112
+ }
1081
1113
  var ENDPOINTS = {
1082
1114
  sandbox: {
1083
1115
  hiofuOrigin: "https://sandbox.hiofu.com",
@@ -1110,15 +1142,24 @@ function assertPublicKey(environment, publicKey) {
1110
1142
  }
1111
1143
  }
1112
1144
  function toApplyOptions(payload) {
1145
+ const jobId = payload.jobId ?? payload.externalJobId;
1146
+ if (!jobId) {
1147
+ throw new HiofuApplyError({
1148
+ code: "configuration_error",
1149
+ message: "HIOFU Apply requires a jobId.",
1150
+ correlationId: createCorrelationId(),
1151
+ retryable: false
1152
+ });
1153
+ }
1113
1154
  const locations = payload.role.locations ?? (payload.role.location ? [payload.role.location] : void 0);
1114
1155
  const options = {
1115
- jobId: payload.externalJobId,
1156
+ jobId,
1116
1157
  jobTitle: payload.jobTitle ?? payload.role.title,
1117
1158
  scopes: payload.scopes,
1118
1159
  variationId: payload.variationId,
1119
1160
  idempotencyKey: payload.idempotencyKey,
1120
1161
  role: {
1121
- externalRoleId: payload.externalJobId,
1162
+ externalRoleId: jobId,
1122
1163
  title: payload.role.title,
1123
1164
  description: payload.role.description,
1124
1165
  locations,
@@ -1203,8 +1244,9 @@ function toApplyError(error, correlationId) {
1203
1244
  });
1204
1245
  }
1205
1246
  function createApplyClient(config) {
1206
- assertPublicKey(config.environment, config.publicKey);
1207
- const endpoints = ENDPOINTS[config.environment];
1247
+ const resolvedEnvironment = config.environment ?? inferEnvironment(config.publicKey) ?? "sandbox";
1248
+ assertPublicKey(resolvedEnvironment, config.publicKey);
1249
+ const endpoints = ENDPOINTS[resolvedEnvironment];
1208
1250
  const client = new HiofuClient({
1209
1251
  clientId: config.publicKey,
1210
1252
  hiofuOrigin: config.hiofuOrigin ?? endpoints.hiofuOrigin,
@@ -1233,7 +1275,7 @@ function createApplyClient(config) {
1233
1275
  const result = await client.apply(toApplyOptions(payload));
1234
1276
  const normalized = toNormalizedResult(
1235
1277
  result,
1236
- config.environment,
1278
+ resolvedEnvironment,
1237
1279
  payload.idempotencyKey
1238
1280
  );
1239
1281
  config.onComplete?.(normalized);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HiofuClient, a as HiofuScope, b as HiofuApplyResult, c as HiofuConfig, d as HiofuEvent, e as HiofuManagementConfig, f as HiofuManagementListRolesParams, g as HiofuManagementPaginatedResult, h as HiofuManagementRoleListItem, i as HiofuManagementRole, j as HiofuManagementCreateRoleInput, k as HiofuManagementUpdateRoleInput, l as HiofuManagementRoleStatus, m as HiofuManagementDeveloperSettings, n as HiofuManagementSaveRoleMappingInput, o as HiofuManagementAddRedirectUriInput, p as HiofuApplyOptions, q as HiofuTokenSet } from './client-Ct5xyQMk.cjs';
2
- export { D as DEFAULT_APPLY_SCOPES, r as DEFAULT_AUTHORIZE_SCOPES, s as HiofuApplyRole, t as HiofuManagementEnvironment, u as HiofuManagementEnvironmentMode, v as HiofuManagementRedirectUri, w as HiofuManagementRoleDimension, x as HiofuManagementRoleDimensionSkill, y as HiofuManagementRoleMapping, z as HiofuPartner } from './client-Ct5xyQMk.cjs';
1
+ import { d as HiofuClient, y as HiofuScope, b as HiofuApplyResult, e as HiofuConfig, f as HiofuEvent, h as HiofuManagementConfig, m as HiofuManagementListRolesParams, n as HiofuManagementPaginatedResult, s as HiofuManagementRoleListItem, p as HiofuManagementRole, i as HiofuManagementCreateRoleInput, w as HiofuManagementUpdateRoleInput, u as HiofuManagementRoleStatus, j as HiofuManagementDeveloperSettings, v as HiofuManagementSaveRoleMappingInput, g as HiofuManagementAddRedirectUriInput, H as HiofuApplyOptions, z as HiofuTokenSet } from './client-7yPni-fK.cjs';
2
+ export { D as DEFAULT_APPLY_SCOPES, a as DEFAULT_AUTHORIZE_SCOPES, c as HiofuApplyRole, k as HiofuManagementEnvironment, l as HiofuManagementEnvironmentMode, o as HiofuManagementRedirectUri, q as HiofuManagementRoleDimension, r as HiofuManagementRoleDimensionSkill, t as HiofuManagementRoleMapping, x as HiofuPartner } from './client-7yPni-fK.cjs';
3
3
 
4
4
  /**
5
5
  * Scan the document for `[data-hiofu-apply]` buttons and wire them up to call
@@ -48,10 +48,12 @@ declare class HiofuApplyError extends Error {
48
48
  });
49
49
  }
50
50
  interface HiofuCreateApplyClientConfig {
51
- environment: HiofuEnvironment;
52
- /** Publishable partner key: pk_test_* for sandbox, pk_live_* for production. */
51
+ /** Inferred from publicKey prefix when omitted. */
52
+ environment?: HiofuEnvironment;
53
+ /** Publishable key: pk_test_* for sandbox, pk_live_* for production. */
53
54
  publicKey: string;
54
- redirectUri: string;
55
+ /** Defaults to the HIOFU-hosted callback when omitted. */
56
+ redirectUri?: string;
55
57
  scopes?: HiofuScope[];
56
58
  applyScopes?: HiofuScope[];
57
59
  storage?: HiofuConfig["storage"];
@@ -67,9 +69,12 @@ interface HiofuCreateApplyClientConfig {
67
69
  /** Internal/testing override. Partners should use `environment`. */
68
70
  apiBase?: string;
69
71
  }
70
- interface HiofuApplyPayload {
72
+ interface HiofuApplyPayloadBase {
71
73
  externalEmployerId?: string;
72
- externalJobId: string;
74
+ /**
75
+ * Backwards-compatible alias for jobId. Prefer jobId in new integrations.
76
+ */
77
+ externalJobId?: string;
73
78
  role: {
74
79
  title: string;
75
80
  description?: string;
@@ -88,6 +93,13 @@ interface HiofuApplyPayload {
88
93
  scopes?: HiofuScope[];
89
94
  partnerTags?: Record<string, string>;
90
95
  }
96
+ type HiofuApplyPayload = HiofuApplyPayloadBase & ({
97
+ jobId: string;
98
+ externalJobId?: string;
99
+ } | {
100
+ jobId?: string;
101
+ externalJobId: string;
102
+ });
91
103
  interface HiofuNormalizedApplyResult {
92
104
  applicationId: string;
93
105
  partnerApplicationId?: string;