@hiofu/apply-sdk 0.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/LICENSE +173 -0
- package/README.md +420 -0
- package/dist/chunk-6YSWH4IM.js +647 -0
- package/dist/client-BD0-Mbnq.d.cts +279 -0
- package/dist/client-BD0-Mbnq.d.ts +279 -0
- package/dist/index.cjs +1036 -0
- package/dist/index.d.cts +158 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.global.js +1 -0
- package/dist/index.js +381 -0
- package/dist/react.cjs +748 -0
- package/dist/react.d.cts +20 -0
- package/dist/react.d.ts +20 -0
- package/dist/react.js +97 -0
- package/package.json +70 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
HiofuProvider: () => HiofuProvider,
|
|
24
|
+
useHiofu: () => useHiofu,
|
|
25
|
+
useHiofuApply: () => useHiofuApply
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(react_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/api.ts
|
|
31
|
+
var DEFAULT_API_BASE = "https://api.hiofu.com/api";
|
|
32
|
+
var HiofuApiError = class extends Error {
|
|
33
|
+
constructor(message, status, body) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.body = body;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
async function jsonFetch(url, init = {}) {
|
|
40
|
+
const headers = new Headers(init.headers);
|
|
41
|
+
headers.set("Content-Type", "application/json");
|
|
42
|
+
if (init.token) headers.set("Authorization", `Bearer ${init.token}`);
|
|
43
|
+
const res = await fetch(url, { ...init, headers });
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
let body = text;
|
|
46
|
+
try {
|
|
47
|
+
body = text ? JSON.parse(text) : null;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const message = body?.message?.toString() ?? `Request failed (${res.status})`;
|
|
52
|
+
throw new HiofuApiError(message, res.status, body);
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
async function exchangeCode(config, args) {
|
|
57
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
58
|
+
const json = await jsonFetch(`${apiBase}/oauth/token`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
grant_type: "authorization_code",
|
|
62
|
+
client_id: config.clientId,
|
|
63
|
+
code: args.code,
|
|
64
|
+
code_verifier: args.verifier,
|
|
65
|
+
redirect_uri: args.redirectUri
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
accessToken: json.data.access_token,
|
|
70
|
+
refreshToken: json.data.refresh_token ?? null,
|
|
71
|
+
expiresAt: Date.now() + json.data.expires_in * 1e3,
|
|
72
|
+
scopes: json.data.scope.split(" ").filter(Boolean)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function refreshTokens(config, refreshToken) {
|
|
76
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
77
|
+
const json = await jsonFetch(`${apiBase}/oauth/token`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
grant_type: "refresh_token",
|
|
81
|
+
client_id: config.clientId,
|
|
82
|
+
refresh_token: refreshToken
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
accessToken: json.data.access_token,
|
|
87
|
+
refreshToken: json.data.refresh_token ?? null,
|
|
88
|
+
expiresAt: Date.now() + json.data.expires_in * 1e3,
|
|
89
|
+
scopes: json.data.scope.split(" ").filter(Boolean)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function revokeTokens(config, token) {
|
|
93
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
94
|
+
await jsonFetch(`${apiBase}/oauth/revoke`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify({ token, client_id: config.clientId })
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function fetchProfileBasic(config, token) {
|
|
100
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
101
|
+
return jsonFetch(`${apiBase}/v1/partner/me`, { token });
|
|
102
|
+
}
|
|
103
|
+
async function fetchProfileFull(config, token) {
|
|
104
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
105
|
+
return jsonFetch(`${apiBase}/v1/partner/me/full`, { token });
|
|
106
|
+
}
|
|
107
|
+
async function submitApplication(config, token, body) {
|
|
108
|
+
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
109
|
+
return jsonFetch(`${apiBase}/v1/partner/applications`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
token,
|
|
112
|
+
headers: body.idempotencyKey ? {
|
|
113
|
+
"Idempotency-Key": body.idempotencyKey
|
|
114
|
+
} : void 0,
|
|
115
|
+
body: JSON.stringify(body)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/environment.ts
|
|
120
|
+
var HiofuConfigurationError = class extends Error {
|
|
121
|
+
constructor(code, message, details) {
|
|
122
|
+
super(message);
|
|
123
|
+
this.name = "HiofuConfigurationError";
|
|
124
|
+
this.code = code;
|
|
125
|
+
this.details = details;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var PRODUCTION_ORIGINS = /* @__PURE__ */ new Set([
|
|
129
|
+
"https://hiofu.com",
|
|
130
|
+
"https://www.hiofu.com",
|
|
131
|
+
"https://api.hiofu.com"
|
|
132
|
+
]);
|
|
133
|
+
var SANDBOX_ORIGINS = /* @__PURE__ */ new Set([
|
|
134
|
+
"https://sandbox.hiofu.com",
|
|
135
|
+
"https://api.sandbox.hiofu.com"
|
|
136
|
+
]);
|
|
137
|
+
function keyMode(clientId) {
|
|
138
|
+
if (clientId.startsWith("pk_test_")) return "test";
|
|
139
|
+
if (clientId.startsWith("pk_live_")) return "live";
|
|
140
|
+
throw new HiofuConfigurationError(
|
|
141
|
+
"hiofu.configuration_error",
|
|
142
|
+
"Hiofu clientId must start with pk_test_ or pk_live_.",
|
|
143
|
+
{ clientIdPrefix: clientId.slice(0, 8) }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
function endpointMode(value) {
|
|
147
|
+
let url;
|
|
148
|
+
try {
|
|
149
|
+
url = new URL(value);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new HiofuConfigurationError(
|
|
152
|
+
"hiofu.configuration_error",
|
|
153
|
+
"Hiofu origin and API URLs must be absolute URLs.",
|
|
154
|
+
{ value }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (["localhost", "127.0.0.1", "0.0.0.0"].includes(url.hostname)) {
|
|
158
|
+
return "local";
|
|
159
|
+
}
|
|
160
|
+
if (SANDBOX_ORIGINS.has(url.origin)) return "sandbox";
|
|
161
|
+
if (PRODUCTION_ORIGINS.has(url.origin)) return "production";
|
|
162
|
+
return "unknown";
|
|
163
|
+
}
|
|
164
|
+
function assertMode(mode, endpoint, field, value) {
|
|
165
|
+
if (endpoint === "local" || endpoint === "unknown") return;
|
|
166
|
+
if (mode === "test" && endpoint === "production") {
|
|
167
|
+
throw new HiofuConfigurationError(
|
|
168
|
+
"hiofu.environment_mismatch",
|
|
169
|
+
"pk_test_* client IDs must use sandbox Hiofu origins, not production.",
|
|
170
|
+
{ field, value, expected: "sandbox", received: "production" }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (mode === "live" && endpoint === "sandbox") {
|
|
174
|
+
throw new HiofuConfigurationError(
|
|
175
|
+
"hiofu.environment_mismatch",
|
|
176
|
+
"pk_live_* client IDs must use production Hiofu origins, not sandbox.",
|
|
177
|
+
{ field, value, expected: "production", received: "sandbox" }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function validateEnvironmentConfig(config) {
|
|
182
|
+
const mode = keyMode(config.clientId);
|
|
183
|
+
assertMode(mode, endpointMode(config.hiofuOrigin), "hiofuOrigin", config.hiofuOrigin);
|
|
184
|
+
assertMode(mode, endpointMode(config.apiBase), "apiBase", config.apiBase);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/pkce.ts
|
|
188
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
189
|
+
function generateVerifier(length = 64) {
|
|
190
|
+
if (length < 43 || length > 128) {
|
|
191
|
+
throw new Error("PKCE verifier length must be 43..128");
|
|
192
|
+
}
|
|
193
|
+
const buf = new Uint8Array(length);
|
|
194
|
+
crypto.getRandomValues(buf);
|
|
195
|
+
let out = "";
|
|
196
|
+
for (let i = 0; i < length; i++) {
|
|
197
|
+
out += CHARSET[buf[i] % CHARSET.length];
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function base64urlEncode(bytes) {
|
|
202
|
+
const u8 = new Uint8Array(bytes);
|
|
203
|
+
let str = "";
|
|
204
|
+
for (const b of u8) str += String.fromCharCode(b);
|
|
205
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
206
|
+
}
|
|
207
|
+
async function generateChallenge(verifier) {
|
|
208
|
+
const data = new TextEncoder().encode(verifier);
|
|
209
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
210
|
+
return base64urlEncode(digest);
|
|
211
|
+
}
|
|
212
|
+
function generateState(length = 43) {
|
|
213
|
+
return generateVerifier(length);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/popup.ts
|
|
217
|
+
var DEFAULT_HIOFU_ORIGIN = "https://hiofu.com";
|
|
218
|
+
var DEFAULT_AUTHORIZE_TIMEOUT_MS = 5 * 6e4;
|
|
219
|
+
function resolveRedirectUri(config, hiofuOrigin) {
|
|
220
|
+
if (config.redirectUri) return config.redirectUri;
|
|
221
|
+
const hiofuBaseOrigin = new URL(hiofuOrigin).origin;
|
|
222
|
+
if (typeof window !== "undefined" && window.location.origin && window.location.origin !== hiofuBaseOrigin) {
|
|
223
|
+
return new URL("/oauth/callback.html", window.location.origin).toString();
|
|
224
|
+
}
|
|
225
|
+
return `${hiofuBaseOrigin}/oauth/callback-shim`;
|
|
226
|
+
}
|
|
227
|
+
async function authorize(config, scopes, context, callbacks) {
|
|
228
|
+
const hiofuOrigin = config.hiofuOrigin ?? DEFAULT_HIOFU_ORIGIN;
|
|
229
|
+
const hiofuBaseOrigin = new URL(hiofuOrigin).origin;
|
|
230
|
+
const redirectUri = resolveRedirectUri(config, hiofuOrigin);
|
|
231
|
+
const authorizeTimeoutMs = config.authorizeTimeoutMs ?? DEFAULT_AUTHORIZE_TIMEOUT_MS;
|
|
232
|
+
const verifier = generateVerifier();
|
|
233
|
+
const challenge = await generateChallenge(verifier);
|
|
234
|
+
const state = generateState();
|
|
235
|
+
const url = new URL(`${hiofuOrigin}/oauth/consent`);
|
|
236
|
+
url.searchParams.set("client_id", config.clientId);
|
|
237
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
238
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
239
|
+
url.searchParams.set("state", state);
|
|
240
|
+
url.searchParams.set("code_challenge", challenge);
|
|
241
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
242
|
+
if (context?.jobId) url.searchParams.set("job_id", context.jobId);
|
|
243
|
+
if (context?.jobTitle) url.searchParams.set("job_title", context.jobTitle);
|
|
244
|
+
if (context?.employerId)
|
|
245
|
+
url.searchParams.set("employer_id", context.employerId);
|
|
246
|
+
if (context?.employerName)
|
|
247
|
+
url.searchParams.set("employer_name", context.employerName);
|
|
248
|
+
if (context?.variationId)
|
|
249
|
+
url.searchParams.set("variation_id", context.variationId);
|
|
250
|
+
const w = 480;
|
|
251
|
+
const h = 720;
|
|
252
|
+
const left = window.screenX + (window.outerWidth - w) / 2;
|
|
253
|
+
const top = window.screenY + (window.outerHeight - h) / 2;
|
|
254
|
+
const features = `popup=yes,width=${w},height=${h},left=${left},top=${top},scrollbars=yes`;
|
|
255
|
+
const popup = window.open(url.toString(), "hiofu_apply", features);
|
|
256
|
+
if (!popup) {
|
|
257
|
+
throw new Error("Popup blocked. Allow popups for this site to continue.");
|
|
258
|
+
}
|
|
259
|
+
callbacks?.onPopupOpened?.({ redirectUri });
|
|
260
|
+
try {
|
|
261
|
+
localStorage.removeItem("hiofu_oauth_result");
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
let settled = false;
|
|
266
|
+
let focusTimer = null;
|
|
267
|
+
let overallTimeout = null;
|
|
268
|
+
let channel = null;
|
|
269
|
+
const cleanup = () => {
|
|
270
|
+
settled = true;
|
|
271
|
+
window.removeEventListener("message", onMessage);
|
|
272
|
+
window.removeEventListener("storage", onStorage);
|
|
273
|
+
window.removeEventListener("focus", onFocus);
|
|
274
|
+
window.clearInterval(poll);
|
|
275
|
+
if (focusTimer) clearTimeout(focusTimer);
|
|
276
|
+
if (overallTimeout) clearTimeout(overallTimeout);
|
|
277
|
+
if (channel) {
|
|
278
|
+
try {
|
|
279
|
+
channel.close();
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
localStorage.removeItem("hiofu_oauth_result");
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const handleResult = (data, channel2) => {
|
|
289
|
+
if (settled) return;
|
|
290
|
+
if (!data || data.type !== "hiofu_oauth") return;
|
|
291
|
+
const stateMatches = data.state === state;
|
|
292
|
+
callbacks?.onPopupResultReceived?.({
|
|
293
|
+
channel: channel2,
|
|
294
|
+
hasCode: Boolean(data.code),
|
|
295
|
+
hasError: Boolean(data.error),
|
|
296
|
+
stateMatches,
|
|
297
|
+
error: data.error ?? null,
|
|
298
|
+
variationId: data.variationId ?? null
|
|
299
|
+
});
|
|
300
|
+
if (!stateMatches) return;
|
|
301
|
+
if (data.error) {
|
|
302
|
+
cleanup();
|
|
303
|
+
try {
|
|
304
|
+
popup.close();
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
callbacks?.onPopupClosed?.(
|
|
308
|
+
data.error === "access_denied" ? "denied" : "completed"
|
|
309
|
+
);
|
|
310
|
+
reject(new Error(data.error));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (data.code) {
|
|
314
|
+
cleanup();
|
|
315
|
+
try {
|
|
316
|
+
popup.close();
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
callbacks?.onPopupClosed?.("completed");
|
|
320
|
+
resolve({
|
|
321
|
+
code: data.code,
|
|
322
|
+
state,
|
|
323
|
+
verifier,
|
|
324
|
+
redirectUri,
|
|
325
|
+
variationId: data.variationId ?? null
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const redirectOrigin = new URL(redirectUri).origin;
|
|
330
|
+
const allowedOrigins = /* @__PURE__ */ new Set([hiofuBaseOrigin, redirectOrigin]);
|
|
331
|
+
const onMessage = (event) => {
|
|
332
|
+
if (settled) return;
|
|
333
|
+
if (!allowedOrigins.has(event.origin)) return;
|
|
334
|
+
handleResult(event.data, "message");
|
|
335
|
+
};
|
|
336
|
+
const onStorage = (event) => {
|
|
337
|
+
if (settled) return;
|
|
338
|
+
if (event.key !== "hiofu_oauth_result" || !event.newValue) return;
|
|
339
|
+
try {
|
|
340
|
+
handleResult(JSON.parse(event.newValue), "storage");
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
345
|
+
try {
|
|
346
|
+
channel = new BroadcastChannel("hiofu_oauth");
|
|
347
|
+
channel.onmessage = (event) => {
|
|
348
|
+
if (settled) return;
|
|
349
|
+
handleResult(event.data, "broadcast");
|
|
350
|
+
};
|
|
351
|
+
} catch {
|
|
352
|
+
channel = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const onFocus = () => {
|
|
356
|
+
if (settled) return;
|
|
357
|
+
if (focusTimer) clearTimeout(focusTimer);
|
|
358
|
+
focusTimer = setTimeout(() => {
|
|
359
|
+
try {
|
|
360
|
+
const raw = localStorage.getItem("hiofu_oauth_result");
|
|
361
|
+
if (raw) {
|
|
362
|
+
handleResult(JSON.parse(raw), "focus_storage");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}, 3e3);
|
|
368
|
+
};
|
|
369
|
+
window.addEventListener("message", onMessage);
|
|
370
|
+
window.addEventListener("storage", onStorage);
|
|
371
|
+
window.addEventListener("focus", onFocus);
|
|
372
|
+
const poll = window.setInterval(() => {
|
|
373
|
+
if (settled) return;
|
|
374
|
+
try {
|
|
375
|
+
const href = popup.location.href;
|
|
376
|
+
if (href && href.includes("code=")) {
|
|
377
|
+
const popupUrl = new URL(href);
|
|
378
|
+
const code = popupUrl.searchParams.get("code");
|
|
379
|
+
const popupState = popupUrl.searchParams.get("state");
|
|
380
|
+
const error = popupUrl.searchParams.get("error");
|
|
381
|
+
const variationId = popupUrl.searchParams.get("variation_id");
|
|
382
|
+
if (code || error) {
|
|
383
|
+
handleResult({
|
|
384
|
+
type: "hiofu_oauth",
|
|
385
|
+
code,
|
|
386
|
+
state: popupState,
|
|
387
|
+
error,
|
|
388
|
+
variationId
|
|
389
|
+
}, "poll_url");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const raw = localStorage.getItem("hiofu_oauth_result");
|
|
397
|
+
if (raw) {
|
|
398
|
+
handleResult(JSON.parse(raw), "poll_storage");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
if (popup.closed) {
|
|
404
|
+
cleanup();
|
|
405
|
+
callbacks?.onPopupClosed?.("user_closed");
|
|
406
|
+
reject(new Error("Authorization popup closed before completion."));
|
|
407
|
+
}
|
|
408
|
+
}, 500);
|
|
409
|
+
overallTimeout = window.setTimeout(() => {
|
|
410
|
+
if (settled) return;
|
|
411
|
+
cleanup();
|
|
412
|
+
try {
|
|
413
|
+
popup.close();
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
callbacks?.onPopupClosed?.("timed_out");
|
|
417
|
+
reject(new Error("Authorization timed out. Please try again."));
|
|
418
|
+
}, authorizeTimeoutMs);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/storage.ts
|
|
423
|
+
var memory = /* @__PURE__ */ new Map();
|
|
424
|
+
function readToken(clientId, storage) {
|
|
425
|
+
if (storage === "memory") return memory.get(clientId) ?? null;
|
|
426
|
+
if (typeof window === "undefined") return null;
|
|
427
|
+
try {
|
|
428
|
+
const raw = window.sessionStorage.getItem(`hiofu:${clientId}:token`);
|
|
429
|
+
if (!raw) return null;
|
|
430
|
+
return JSON.parse(raw);
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function writeToken(clientId, storage, token) {
|
|
436
|
+
if (storage === "memory") {
|
|
437
|
+
memory.set(clientId, token);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (typeof window === "undefined") return;
|
|
441
|
+
try {
|
|
442
|
+
window.sessionStorage.setItem(
|
|
443
|
+
`hiofu:${clientId}:token`,
|
|
444
|
+
JSON.stringify(token)
|
|
445
|
+
);
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function clearToken(clientId, storage) {
|
|
450
|
+
if (storage === "memory") {
|
|
451
|
+
memory.delete(clientId);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (typeof window === "undefined") return;
|
|
455
|
+
try {
|
|
456
|
+
window.sessionStorage.removeItem(`hiofu:${clientId}:token`);
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/client.ts
|
|
462
|
+
var DEFAULT_AUTHORIZE_SCOPES = [
|
|
463
|
+
"profile.basic"
|
|
464
|
+
];
|
|
465
|
+
var DEFAULT_APPLY_SCOPES = [
|
|
466
|
+
"applications.write",
|
|
467
|
+
"passport.snapshot"
|
|
468
|
+
];
|
|
469
|
+
function dedupeScopes(scopes) {
|
|
470
|
+
return [...new Set(scopes)];
|
|
471
|
+
}
|
|
472
|
+
function normalizeApplyScopes(scopes) {
|
|
473
|
+
const normalized = [...scopes];
|
|
474
|
+
if (!normalized.includes("applications.write")) {
|
|
475
|
+
normalized.push("applications.write");
|
|
476
|
+
}
|
|
477
|
+
if (!normalized.includes("passport.snapshot")) {
|
|
478
|
+
normalized.push("passport.snapshot");
|
|
479
|
+
}
|
|
480
|
+
return dedupeScopes(normalized);
|
|
481
|
+
}
|
|
482
|
+
function generateIdempotencyKey() {
|
|
483
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
484
|
+
return `hiofu_${crypto.randomUUID()}`;
|
|
485
|
+
}
|
|
486
|
+
return `hiofu_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
487
|
+
}
|
|
488
|
+
function toPublicToken(token) {
|
|
489
|
+
return {
|
|
490
|
+
expiresAt: token.expiresAt,
|
|
491
|
+
scopes: [...token.scopes]
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
var HiofuClient = class {
|
|
495
|
+
constructor(config) {
|
|
496
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
497
|
+
if (!config.clientId) {
|
|
498
|
+
throw new Error("HiofuClient: clientId is required");
|
|
499
|
+
}
|
|
500
|
+
this.config = {
|
|
501
|
+
...config,
|
|
502
|
+
hiofuOrigin: config.hiofuOrigin ?? "https://hiofu.com",
|
|
503
|
+
apiBase: config.apiBase ?? "https://api.hiofu.com/api",
|
|
504
|
+
storage: config.storage ?? "memory",
|
|
505
|
+
authorizeTimeoutMs: config.authorizeTimeoutMs ?? 5 * 6e4
|
|
506
|
+
};
|
|
507
|
+
validateEnvironmentConfig(this.config);
|
|
508
|
+
}
|
|
509
|
+
subscribe(listener) {
|
|
510
|
+
this.listeners.add(listener);
|
|
511
|
+
return () => {
|
|
512
|
+
this.listeners.delete(listener);
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
emit(event) {
|
|
516
|
+
try {
|
|
517
|
+
this.config.onEvent?.(event);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
for (const listener of this.listeners) {
|
|
521
|
+
try {
|
|
522
|
+
listener(event);
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Returns a usable access token, refreshing if needed. */
|
|
528
|
+
async getAccessToken() {
|
|
529
|
+
const token = readToken(this.config.clientId, this.config.storage);
|
|
530
|
+
if (!token) return null;
|
|
531
|
+
if (token.expiresAt - 3e4 > Date.now()) return token.accessToken;
|
|
532
|
+
if (!token.refreshToken) return null;
|
|
533
|
+
try {
|
|
534
|
+
const refreshed = await refreshTokens(this.config, token.refreshToken);
|
|
535
|
+
writeToken(this.config.clientId, this.config.storage, refreshed);
|
|
536
|
+
this.emit({ type: "token_issued", token: toPublicToken(refreshed) });
|
|
537
|
+
return refreshed.accessToken;
|
|
538
|
+
} catch {
|
|
539
|
+
clearToken(this.config.clientId, this.config.storage);
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/** Open the popup, get a token. */
|
|
544
|
+
async authorize(scopes = this.config.scopes ?? DEFAULT_AUTHORIZE_SCOPES, context) {
|
|
545
|
+
const result = await authorize(this.config, dedupeScopes(scopes), context, {
|
|
546
|
+
onPopupOpened: ({ redirectUri }) => {
|
|
547
|
+
this.emit({ type: "popup_opened", redirectUri });
|
|
548
|
+
},
|
|
549
|
+
onPopupClosed: (reason) => {
|
|
550
|
+
this.emit({ type: "popup_closed", reason });
|
|
551
|
+
},
|
|
552
|
+
onPopupResultReceived: (details) => {
|
|
553
|
+
this.emit({ type: "popup_result_received", ...details });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
const token = await exchangeCode(this.config, {
|
|
557
|
+
code: result.code,
|
|
558
|
+
verifier: result.verifier,
|
|
559
|
+
redirectUri: result.redirectUri
|
|
560
|
+
});
|
|
561
|
+
writeToken(this.config.clientId, this.config.storage, token);
|
|
562
|
+
const publicToken = toPublicToken(token);
|
|
563
|
+
this.emit({ type: "token_issued", token: publicToken });
|
|
564
|
+
return publicToken;
|
|
565
|
+
}
|
|
566
|
+
/** End-to-end apply: authorize if needed, then POST application. */
|
|
567
|
+
async apply(opts) {
|
|
568
|
+
this.emit({
|
|
569
|
+
type: "apply_started",
|
|
570
|
+
input: {
|
|
571
|
+
jobId: opts.jobId,
|
|
572
|
+
jobTitle: opts.jobTitle,
|
|
573
|
+
employerId: opts.employerId,
|
|
574
|
+
employerName: opts.employerName,
|
|
575
|
+
variationId: opts.variationId ?? void 0
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
try {
|
|
579
|
+
let access = await this.getAccessToken();
|
|
580
|
+
let token = readToken(
|
|
581
|
+
this.config.clientId,
|
|
582
|
+
this.config.storage
|
|
583
|
+
);
|
|
584
|
+
const needScopes = normalizeApplyScopes(
|
|
585
|
+
opts.scopes ?? this.config.applyScopes ?? this.config.scopes ?? DEFAULT_APPLY_SCOPES
|
|
586
|
+
);
|
|
587
|
+
const haveAllScopes = token && needScopes.every((scope) => token.scopes.includes(scope));
|
|
588
|
+
const requireApplicationReview = !opts.variationId;
|
|
589
|
+
const idempotencyKey = opts.idempotencyKey?.trim() || generateIdempotencyKey();
|
|
590
|
+
let selectedVariationId = opts.variationId ?? null;
|
|
591
|
+
if (!access || !haveAllScopes || requireApplicationReview) {
|
|
592
|
+
const auth = await authorize(this.config, needScopes, {
|
|
593
|
+
jobId: opts.jobId,
|
|
594
|
+
jobTitle: opts.jobTitle,
|
|
595
|
+
employerId: opts.employerId,
|
|
596
|
+
employerName: opts.employerName,
|
|
597
|
+
variationId: opts.variationId
|
|
598
|
+
}, {
|
|
599
|
+
onPopupOpened: ({ redirectUri }) => {
|
|
600
|
+
this.emit({ type: "popup_opened", redirectUri });
|
|
601
|
+
},
|
|
602
|
+
onPopupClosed: (reason) => {
|
|
603
|
+
this.emit({ type: "popup_closed", reason });
|
|
604
|
+
},
|
|
605
|
+
onPopupResultReceived: (details) => {
|
|
606
|
+
this.emit({ type: "popup_result_received", ...details });
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const exchanged = await exchangeCode(this.config, {
|
|
610
|
+
code: auth.code,
|
|
611
|
+
verifier: auth.verifier,
|
|
612
|
+
redirectUri: auth.redirectUri
|
|
613
|
+
});
|
|
614
|
+
writeToken(this.config.clientId, this.config.storage, exchanged);
|
|
615
|
+
this.emit({ type: "token_issued", token: toPublicToken(exchanged) });
|
|
616
|
+
token = exchanged;
|
|
617
|
+
access = exchanged.accessToken;
|
|
618
|
+
selectedVariationId = auth.variationId ?? opts.variationId ?? null;
|
|
619
|
+
}
|
|
620
|
+
this.emit({
|
|
621
|
+
type: "apply_submitting",
|
|
622
|
+
idempotencyKey,
|
|
623
|
+
variationId: selectedVariationId
|
|
624
|
+
});
|
|
625
|
+
const json = await submitApplication(this.config, access, {
|
|
626
|
+
jobId: opts.jobId,
|
|
627
|
+
jobTitle: opts.jobTitle,
|
|
628
|
+
employerId: opts.employerId,
|
|
629
|
+
employerName: opts.employerName,
|
|
630
|
+
role: opts.role,
|
|
631
|
+
variationId: selectedVariationId ?? void 0,
|
|
632
|
+
idempotencyKey
|
|
633
|
+
});
|
|
634
|
+
const result = {
|
|
635
|
+
application: json.data.application,
|
|
636
|
+
authorization: toPublicToken(token),
|
|
637
|
+
idempotencyKey,
|
|
638
|
+
sharedView: json.data.sharedView ?? (selectedVariationId ? { type: "variation", label: null } : { type: "full_passport", label: null }),
|
|
639
|
+
versionNumber: json.data.versionNumber ?? null,
|
|
640
|
+
profile: json.data.profile ?? null,
|
|
641
|
+
evidence: json.data.evidence ?? null,
|
|
642
|
+
snapshot: json.data.snapshot ?? null,
|
|
643
|
+
delivery: json.data.delivery ?? null
|
|
644
|
+
};
|
|
645
|
+
this.emit({ type: "apply_success", result });
|
|
646
|
+
return result;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
649
|
+
this.emit({ type: "apply_error", error: err });
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async getProfile(scope = "basic") {
|
|
654
|
+
const token = await this.getAccessToken();
|
|
655
|
+
if (!token) throw new Error("Not authorised. Call authorize() or apply().");
|
|
656
|
+
return scope === "full" ? fetchProfileFull(this.config, token) : fetchProfileBasic(this.config, token);
|
|
657
|
+
}
|
|
658
|
+
async logout() {
|
|
659
|
+
const token = readToken(this.config.clientId, this.config.storage);
|
|
660
|
+
if (token?.accessToken) {
|
|
661
|
+
try {
|
|
662
|
+
await revokeTokens(this.config, token.accessToken);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
clearToken(this.config.clientId, this.config.storage);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/react.ts
|
|
671
|
+
var Ctx = (0, import_react.createContext)(null);
|
|
672
|
+
function HiofuProvider({ config, children }) {
|
|
673
|
+
const client = (0, import_react.useMemo)(() => new HiofuClient(config), [
|
|
674
|
+
config.clientId,
|
|
675
|
+
config.hiofuOrigin,
|
|
676
|
+
config.apiBase,
|
|
677
|
+
config.redirectUri,
|
|
678
|
+
config.storage,
|
|
679
|
+
config.applyScopes?.join(","),
|
|
680
|
+
config.scopes?.join(","),
|
|
681
|
+
config.authorizeTimeoutMs,
|
|
682
|
+
config.onEvent
|
|
683
|
+
]);
|
|
684
|
+
return (0, import_react.createElement)(Ctx.Provider, { value: client }, children);
|
|
685
|
+
}
|
|
686
|
+
function useHiofu() {
|
|
687
|
+
const client = (0, import_react.useContext)(Ctx);
|
|
688
|
+
if (!client) {
|
|
689
|
+
throw new Error("useHiofu must be used inside <HiofuProvider>");
|
|
690
|
+
}
|
|
691
|
+
return client;
|
|
692
|
+
}
|
|
693
|
+
function useHiofuApply() {
|
|
694
|
+
const client = useHiofu();
|
|
695
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
696
|
+
const [result, setResult] = (0, import_react.useState)(null);
|
|
697
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
698
|
+
(0, import_react.useEffect)(() => {
|
|
699
|
+
return client.subscribe((event) => {
|
|
700
|
+
switch (event.type) {
|
|
701
|
+
case "apply_started":
|
|
702
|
+
setStatus("authorising");
|
|
703
|
+
setResult(null);
|
|
704
|
+
setError(null);
|
|
705
|
+
break;
|
|
706
|
+
case "apply_submitting":
|
|
707
|
+
setStatus("submitting");
|
|
708
|
+
break;
|
|
709
|
+
case "apply_success":
|
|
710
|
+
setStatus("success");
|
|
711
|
+
setResult(event.result);
|
|
712
|
+
setError(null);
|
|
713
|
+
break;
|
|
714
|
+
case "apply_error":
|
|
715
|
+
setStatus("error");
|
|
716
|
+
setError(event.error);
|
|
717
|
+
break;
|
|
718
|
+
default:
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}, [client]);
|
|
723
|
+
const apply = (0, import_react.useCallback)(
|
|
724
|
+
async (opts) => {
|
|
725
|
+
setStatus("authorising");
|
|
726
|
+
setError(null);
|
|
727
|
+
setResult(null);
|
|
728
|
+
try {
|
|
729
|
+
return await client.apply(opts);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
732
|
+
setError(e);
|
|
733
|
+
setStatus("error");
|
|
734
|
+
throw e;
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
[client]
|
|
738
|
+
);
|
|
739
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
740
|
+
setStatus("idle");
|
|
741
|
+
setResult(null);
|
|
742
|
+
setError(null);
|
|
743
|
+
}, []);
|
|
744
|
+
(0, import_react.useEffect)(() => {
|
|
745
|
+
void client.getAccessToken();
|
|
746
|
+
}, [client]);
|
|
747
|
+
return { status, result, error, apply, reset };
|
|
748
|
+
}
|