@iqauth/sdk 2.6.4 → 2.8.1
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 +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +212 -46
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +293 -34
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
- package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/chunk-VYQ3ETCK.mjs +244 -0
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/chunk-WHT6WKTY.mjs +3180 -0
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/chunk-WSH4SW7F.mjs +490 -0
- package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
- package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +563 -85
- package/dist/express.mjs +73 -34
- package/dist/fastify.d.mts +10 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +589 -65
- package/dist/fastify.mjs +101 -11
- package/dist/hono.d.mts +10 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +566 -65
- package/dist/hono.mjs +78 -11
- package/dist/index-Cko-d5po.d.mts +1848 -0
- package/dist/index-RNqwEcmY.d.ts +1848 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +694 -75
- package/dist/index.mjs +30 -10
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +307 -46
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +596 -205
- package/dist/next.mjs +83 -10
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +98 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +882 -73
- package/dist/react.mjs +71 -2631
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +200 -4
- package/dist/server/handlers.d.ts +200 -4
- package/dist/server/handlers.js +530 -16
- package/dist/server/handlers.mjs +14 -3
- package/dist/server.d.mts +171 -8
- package/dist/server.d.ts +171 -8
- package/dist/server.js +579 -61
- package/dist/server.mjs +99 -12
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +212 -46
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
- package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
- package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
- package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
- package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
- package/dist/webhooks.d.mts +113 -17
- package/dist/webhooks.d.ts +113 -17
- package/dist/webhooks.js +179 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/docs/guides/invitations.md +65 -0
- package/package.json +19 -4
- package/dist/chunk-6TDJJER7.mjs +0 -217
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/hono.js
CHANGED
|
@@ -35,13 +35,30 @@ __export(hono_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(hono_exports);
|
|
36
36
|
|
|
37
37
|
// src/errors.ts
|
|
38
|
-
var IQAuthError = class extends Error {
|
|
39
|
-
constructor(code, message, status,
|
|
38
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
39
|
+
constructor(code, message, status, cause) {
|
|
40
40
|
super(message);
|
|
41
41
|
this.name = "IQAuthError";
|
|
42
42
|
this.code = code;
|
|
43
43
|
this.status = status;
|
|
44
|
-
this.
|
|
44
|
+
this.cause = cause;
|
|
45
|
+
this.raw = cause;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
49
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
50
|
+
*/
|
|
51
|
+
static isIQAuthError(value) {
|
|
52
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Type-narrowed code check. Lets callers write
|
|
56
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
57
|
+
* taxonomy without losing the ability to handle server codes via
|
|
58
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
59
|
+
*/
|
|
60
|
+
is(code) {
|
|
61
|
+
return this.code === code;
|
|
45
62
|
}
|
|
46
63
|
};
|
|
47
64
|
|
|
@@ -156,7 +173,7 @@ var HttpClient = class {
|
|
|
156
173
|
headers: this.buildHeaders(),
|
|
157
174
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
158
175
|
const refreshToken = this.config.getRefreshToken();
|
|
159
|
-
if (!refreshToken) throw new IQAuthError("
|
|
176
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
160
177
|
return { body: JSON.stringify({ refreshToken }) };
|
|
161
178
|
})()
|
|
162
179
|
});
|
|
@@ -173,7 +190,7 @@ var HttpClient = class {
|
|
|
173
190
|
return;
|
|
174
191
|
}
|
|
175
192
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
176
|
-
throw new IQAuthError("
|
|
193
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
177
194
|
}
|
|
178
195
|
const tokens = {
|
|
179
196
|
accessToken: body.data.accessToken,
|
|
@@ -191,7 +208,7 @@ var HttpClient = class {
|
|
|
191
208
|
return this.requestWithRetry(method, path, body, options, false);
|
|
192
209
|
}
|
|
193
210
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
194
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
211
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
195
212
|
await this.attemptRefresh();
|
|
196
213
|
}
|
|
197
214
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -278,17 +295,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
278
295
|
tenants: data.tenants
|
|
279
296
|
};
|
|
280
297
|
}
|
|
298
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
299
|
+
return {
|
|
300
|
+
status: "scope_selection",
|
|
301
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
302
|
+
tenantId: data.tenantId,
|
|
303
|
+
scopes: data.scopes
|
|
304
|
+
};
|
|
305
|
+
}
|
|
281
306
|
throw new Error("Unexpected login response shape");
|
|
282
307
|
}
|
|
283
308
|
var AuthModule = class {
|
|
284
309
|
constructor(http) {
|
|
285
310
|
this.http = http;
|
|
286
311
|
}
|
|
287
|
-
async login(email, password) {
|
|
312
|
+
async login(email, password, opts) {
|
|
313
|
+
const body = { email, password };
|
|
314
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
288
315
|
const data = await this.http.request(
|
|
289
316
|
"POST",
|
|
290
317
|
"/api/v1/auth/login",
|
|
291
|
-
|
|
318
|
+
body,
|
|
292
319
|
{ skipAutoRefresh: true }
|
|
293
320
|
);
|
|
294
321
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -326,13 +353,29 @@ var AuthModule = class {
|
|
|
326
353
|
method
|
|
327
354
|
}, { skipAutoRefresh: true });
|
|
328
355
|
}
|
|
329
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
356
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
357
|
+
const body = { tenantSelectionToken, tenantId };
|
|
358
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
330
359
|
const data = await this.http.request(
|
|
331
360
|
"POST",
|
|
332
361
|
"/api/v1/auth/select-tenant",
|
|
362
|
+
body,
|
|
363
|
+
{ skipAutoRefresh: true }
|
|
364
|
+
);
|
|
365
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
369
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
370
|
+
* returned in the prior `scope_selection` envelope.
|
|
371
|
+
*/
|
|
372
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
373
|
+
const data = await this.http.request(
|
|
374
|
+
"POST",
|
|
375
|
+
"/api/v1/auth/select-scope",
|
|
333
376
|
{
|
|
334
|
-
|
|
335
|
-
|
|
377
|
+
scopeSelectionToken,
|
|
378
|
+
membershipId
|
|
336
379
|
},
|
|
337
380
|
{ skipAutoRefresh: true }
|
|
338
381
|
);
|
|
@@ -419,6 +462,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
419
462
|
"iqvalidate"
|
|
420
463
|
];
|
|
421
464
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
465
|
+
function classifyJoseError(err) {
|
|
466
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
467
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
468
|
+
}
|
|
469
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
470
|
+
return { code: "token_invalid", message: err.message };
|
|
471
|
+
}
|
|
472
|
+
if (err instanceof Error) {
|
|
473
|
+
return { code: "token_invalid", message: err.message };
|
|
474
|
+
}
|
|
475
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
476
|
+
}
|
|
422
477
|
function decodeProtectedHeader(token) {
|
|
423
478
|
const parts = token.split(".");
|
|
424
479
|
if (parts.length < 2) return null;
|
|
@@ -455,11 +510,11 @@ var TokensModule = class {
|
|
|
455
510
|
async verify(token, options = {}) {
|
|
456
511
|
const header = decodeProtectedHeader(token);
|
|
457
512
|
if (!header) {
|
|
458
|
-
throw new IQAuthError("
|
|
513
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
459
514
|
}
|
|
460
515
|
const kid = header.kid;
|
|
461
516
|
if (!kid) {
|
|
462
|
-
throw new IQAuthError("
|
|
517
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
463
518
|
}
|
|
464
519
|
let cache = await this.ensureCache();
|
|
465
520
|
if (!cache.byKid.has(kid)) {
|
|
@@ -467,7 +522,7 @@ var TokensModule = class {
|
|
|
467
522
|
cache = await this.ensureCache();
|
|
468
523
|
}
|
|
469
524
|
if (!cache.byKid.has(kid)) {
|
|
470
|
-
throw new IQAuthError("
|
|
525
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
471
526
|
}
|
|
472
527
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
473
528
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -483,16 +538,8 @@ var TokensModule = class {
|
|
|
483
538
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
484
539
|
return payload;
|
|
485
540
|
} catch (err) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
490
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
491
|
-
}
|
|
492
|
-
if (err instanceof Error) {
|
|
493
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
494
|
-
}
|
|
495
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
541
|
+
const classified = classifyJoseError(err);
|
|
542
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
496
543
|
}
|
|
497
544
|
}
|
|
498
545
|
/**
|
|
@@ -534,7 +581,7 @@ var TokensModule = class {
|
|
|
534
581
|
getClaims(token) {
|
|
535
582
|
const claims = this.decode(token);
|
|
536
583
|
if (!claims) {
|
|
537
|
-
throw new IQAuthError("
|
|
584
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
538
585
|
}
|
|
539
586
|
return claims;
|
|
540
587
|
}
|
|
@@ -544,7 +591,7 @@ var TokensModule = class {
|
|
|
544
591
|
}
|
|
545
592
|
await this.refreshJwks();
|
|
546
593
|
if (!this.jwksCache) {
|
|
547
|
-
throw new IQAuthError("
|
|
594
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
548
595
|
}
|
|
549
596
|
return this.jwksCache;
|
|
550
597
|
}
|
|
@@ -554,22 +601,38 @@ var TokensModule = class {
|
|
|
554
601
|
}
|
|
555
602
|
this.inFlightRefresh = (async () => {
|
|
556
603
|
try {
|
|
557
|
-
|
|
604
|
+
let res;
|
|
605
|
+
try {
|
|
606
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
throw new IQAuthError(
|
|
609
|
+
"network",
|
|
610
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
611
|
+
void 0,
|
|
612
|
+
err
|
|
613
|
+
);
|
|
614
|
+
}
|
|
558
615
|
if (!res.ok) {
|
|
559
616
|
throw new IQAuthError(
|
|
560
|
-
"
|
|
561
|
-
`Failed to fetch JWKS: ${res.status}
|
|
617
|
+
"jwks_fetch_failed",
|
|
618
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
619
|
+
res.status
|
|
562
620
|
);
|
|
563
621
|
}
|
|
564
622
|
let jwks;
|
|
565
623
|
try {
|
|
566
624
|
jwks = await res.json();
|
|
567
|
-
} catch {
|
|
568
|
-
throw new IQAuthError(
|
|
625
|
+
} catch (err) {
|
|
626
|
+
throw new IQAuthError(
|
|
627
|
+
"jwks_fetch_failed",
|
|
628
|
+
"Malformed JWKS response: invalid JSON",
|
|
629
|
+
res.status,
|
|
630
|
+
err
|
|
631
|
+
);
|
|
569
632
|
}
|
|
570
633
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
571
634
|
throw new IQAuthError(
|
|
572
|
-
"
|
|
635
|
+
"jwks_fetch_failed",
|
|
573
636
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
574
637
|
);
|
|
575
638
|
}
|
|
@@ -577,7 +640,7 @@ var TokensModule = class {
|
|
|
577
640
|
for (const key of jwks.keys) {
|
|
578
641
|
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" && typeof key.x !== "string" || key.kty === "RSA" && (typeof key.n !== "string" || typeof key.e !== "string")) {
|
|
579
642
|
throw new IQAuthError(
|
|
580
|
-
"
|
|
643
|
+
"jwks_fetch_failed",
|
|
581
644
|
"Malformed JWKS response: key missing required fields"
|
|
582
645
|
);
|
|
583
646
|
}
|
|
@@ -595,6 +658,19 @@ var TokensModule = class {
|
|
|
595
658
|
clearCache() {
|
|
596
659
|
this.jwksCache = null;
|
|
597
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
663
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
664
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
665
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
666
|
+
*/
|
|
667
|
+
async prewarm() {
|
|
668
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
669
|
+
try {
|
|
670
|
+
await this.refreshJwks();
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
598
674
|
};
|
|
599
675
|
|
|
600
676
|
// src/modules/sessions.ts
|
|
@@ -918,14 +994,14 @@ var OidcModule = class {
|
|
|
918
994
|
*/
|
|
919
995
|
async handleCallback(params) {
|
|
920
996
|
if (!params.state) {
|
|
921
|
-
throw new IQAuthError("
|
|
997
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
922
998
|
}
|
|
923
999
|
if (!params.code) {
|
|
924
|
-
throw new IQAuthError("
|
|
1000
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
925
1001
|
}
|
|
926
1002
|
const stored = await this.stateStore.get(params.state);
|
|
927
1003
|
if (!stored) {
|
|
928
|
-
throw new IQAuthError("
|
|
1004
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
929
1005
|
}
|
|
930
1006
|
let tokens;
|
|
931
1007
|
try {
|
|
@@ -943,7 +1019,7 @@ var OidcModule = class {
|
|
|
943
1019
|
if (tokens.id_token) {
|
|
944
1020
|
if (!this.tokensModule) {
|
|
945
1021
|
throw new IQAuthError(
|
|
946
|
-
"
|
|
1022
|
+
"config_invalid",
|
|
947
1023
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
948
1024
|
);
|
|
949
1025
|
}
|
|
@@ -954,7 +1030,7 @@ var OidcModule = class {
|
|
|
954
1030
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
955
1031
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
956
1032
|
throw new IQAuthError(
|
|
957
|
-
"
|
|
1033
|
+
"token_invalid",
|
|
958
1034
|
"OIDC id_token nonce did not match the stored value"
|
|
959
1035
|
);
|
|
960
1036
|
}
|
|
@@ -1155,6 +1231,9 @@ var AppsModule = class {
|
|
|
1155
1231
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1156
1232
|
*/
|
|
1157
1233
|
async get(appKey) {
|
|
1234
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1235
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1236
|
+
}
|
|
1158
1237
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1159
1238
|
}
|
|
1160
1239
|
/**
|
|
@@ -1174,6 +1253,16 @@ var AppsModule = class {
|
|
|
1174
1253
|
401
|
|
1175
1254
|
);
|
|
1176
1255
|
}
|
|
1256
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1257
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1258
|
+
}
|
|
1259
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1260
|
+
throw new IQAuthError(
|
|
1261
|
+
"ENVIRONMENT_REQUIRED",
|
|
1262
|
+
"manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
|
|
1263
|
+
400
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1177
1266
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1178
1267
|
}
|
|
1179
1268
|
/**
|
|
@@ -1183,11 +1272,14 @@ var AppsModule = class {
|
|
|
1183
1272
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1184
1273
|
*/
|
|
1185
1274
|
async isRegistered(appKey) {
|
|
1275
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1276
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1277
|
+
}
|
|
1186
1278
|
try {
|
|
1187
1279
|
await this.get(appKey);
|
|
1188
1280
|
return true;
|
|
1189
1281
|
} catch (err) {
|
|
1190
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1282
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1191
1283
|
return false;
|
|
1192
1284
|
}
|
|
1193
1285
|
throw err;
|
|
@@ -1224,6 +1316,20 @@ var RolesModule = class {
|
|
|
1224
1316
|
};
|
|
1225
1317
|
|
|
1226
1318
|
// src/modules/permissionGroups.ts
|
|
1319
|
+
function assertAppKey(appKey, callsite) {
|
|
1320
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1321
|
+
throw new IQAuthError(
|
|
1322
|
+
"VALIDATION_ERROR",
|
|
1323
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1324
|
+
400
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1329
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1330
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1227
1333
|
var PermissionGroupsModule = class {
|
|
1228
1334
|
constructor(http) {
|
|
1229
1335
|
this.http = http;
|
|
@@ -1244,7 +1350,14 @@ var PermissionGroupsModule = class {
|
|
|
1244
1350
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1245
1351
|
}
|
|
1246
1352
|
async addPermission(tenantId, groupId, data) {
|
|
1247
|
-
|
|
1353
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1354
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1355
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1356
|
+
appKey: data.appKey,
|
|
1357
|
+
nodeKey: data.nodeKey,
|
|
1358
|
+
effect: data.effect,
|
|
1359
|
+
weight: data.weight
|
|
1360
|
+
});
|
|
1248
1361
|
}
|
|
1249
1362
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1250
1363
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1268,21 +1381,51 @@ var PermissionGroupsModule = class {
|
|
|
1268
1381
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1269
1382
|
}
|
|
1270
1383
|
async addUserOverride(tenantId, userId, data) {
|
|
1271
|
-
|
|
1384
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1385
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1386
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1387
|
+
appKey: data.appKey,
|
|
1388
|
+
nodeKey: data.nodeKey,
|
|
1389
|
+
effect: data.effect,
|
|
1390
|
+
weight: data.weight,
|
|
1391
|
+
expiresAt: data.expiresAt
|
|
1392
|
+
});
|
|
1272
1393
|
}
|
|
1273
1394
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1274
1395
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1275
1396
|
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1399
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1400
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1401
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1402
|
+
*/
|
|
1276
1403
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const qs = query.toString();
|
|
1281
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1404
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1405
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1406
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1282
1407
|
}
|
|
1283
1408
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1409
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1410
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1284
1411
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1285
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1415
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1416
|
+
* single misconfigured entry can't slip through and silently report
|
|
1417
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1418
|
+
*/
|
|
1419
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1420
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1421
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1422
|
+
}
|
|
1423
|
+
checks.forEach((c, i) => {
|
|
1424
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1425
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1426
|
+
});
|
|
1427
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1428
|
+
}
|
|
1286
1429
|
};
|
|
1287
1430
|
|
|
1288
1431
|
// src/modules/apiKeys.ts
|
|
@@ -1707,6 +1850,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1707
1850
|
this._refreshToken = tokens.refreshToken;
|
|
1708
1851
|
},
|
|
1709
1852
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1853
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1854
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1855
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1856
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1710
1857
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1711
1858
|
sessionHeaderName: config.sessionHeaderName,
|
|
1712
1859
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1747,6 +1894,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1747
1894
|
static forServer(config) {
|
|
1748
1895
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1749
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1899
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1900
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1901
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1902
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1903
|
+
*/
|
|
1750
1904
|
static forMobile(config) {
|
|
1751
1905
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1752
1906
|
}
|
|
@@ -1763,6 +1917,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1763
1917
|
getRefreshToken() {
|
|
1764
1918
|
return this._refreshToken;
|
|
1765
1919
|
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1922
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1923
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1924
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1925
|
+
*/
|
|
1926
|
+
async prewarm() {
|
|
1927
|
+
await Promise.all([
|
|
1928
|
+
this.tokens.prewarm(),
|
|
1929
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1930
|
+
]);
|
|
1931
|
+
}
|
|
1766
1932
|
getCurrentClaims() {
|
|
1767
1933
|
if (!this._accessToken) return null;
|
|
1768
1934
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1803,14 +1969,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1803
1969
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1804
1970
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1805
1971
|
throw new IQAuthError(
|
|
1806
|
-
"
|
|
1972
|
+
"config_invalid",
|
|
1807
1973
|
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
1808
1974
|
);
|
|
1809
1975
|
}
|
|
1810
1976
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1811
1977
|
if (!shapeMatch) {
|
|
1812
1978
|
throw new IQAuthError(
|
|
1813
|
-
"
|
|
1979
|
+
"config_invalid",
|
|
1814
1980
|
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
1815
1981
|
);
|
|
1816
1982
|
}
|
|
@@ -1819,19 +1985,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1819
1985
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1820
1986
|
} catch {
|
|
1821
1987
|
throw new IQAuthError(
|
|
1822
|
-
"
|
|
1988
|
+
"config_invalid",
|
|
1823
1989
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1824
1990
|
);
|
|
1825
1991
|
}
|
|
1826
1992
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1827
1993
|
throw new IQAuthError(
|
|
1828
|
-
"
|
|
1994
|
+
"config_invalid",
|
|
1829
1995
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1830
1996
|
);
|
|
1831
1997
|
}
|
|
1832
1998
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1833
1999
|
throw new IQAuthError(
|
|
1834
|
-
"
|
|
2000
|
+
"config_invalid",
|
|
1835
2001
|
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
1836
2002
|
);
|
|
1837
2003
|
}
|
|
@@ -1844,6 +2010,45 @@ function isPublishableKeyPayload(value) {
|
|
|
1844
2010
|
}
|
|
1845
2011
|
|
|
1846
2012
|
// src/server/handlers.ts
|
|
2013
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2014
|
+
const baseUser = {
|
|
2015
|
+
sub: claims.sub,
|
|
2016
|
+
email: claims.email,
|
|
2017
|
+
name: claims.name,
|
|
2018
|
+
tenantId: claims.tenantId,
|
|
2019
|
+
vendorId: claims.vendorId,
|
|
2020
|
+
roles: claims.roles ?? [],
|
|
2021
|
+
entitlements: claims.entitlements ?? [],
|
|
2022
|
+
// Task #171 — project the active source/client scope onto the userinfo
|
|
2023
|
+
// payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
|
|
2024
|
+
// expose it without consumers having to re-decode the JWT.
|
|
2025
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
2026
|
+
};
|
|
2027
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2028
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2029
|
+
return {
|
|
2030
|
+
success: true,
|
|
2031
|
+
data: {
|
|
2032
|
+
user,
|
|
2033
|
+
claims,
|
|
2034
|
+
tenantId: claims.tenantId ?? null
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
function emitTiming(cfg, event) {
|
|
2039
|
+
if (cfg.debug) {
|
|
2040
|
+
try {
|
|
2041
|
+
console.debug("[iqauth_helper]", event);
|
|
2042
|
+
} catch {
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (cfg.onTimingEvent) {
|
|
2046
|
+
try {
|
|
2047
|
+
cfg.onTimingEvent(event);
|
|
2048
|
+
} catch {
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
1847
2052
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1848
2053
|
"TOKEN_REVOKED",
|
|
1849
2054
|
"SESSION_REVOKED",
|
|
@@ -1862,19 +2067,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
1862
2067
|
}
|
|
1863
2068
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1864
2069
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2070
|
+
function assertCookiePrefixInvariants(name, secure, path, domain) {
|
|
2071
|
+
if (name.startsWith("__Host-")) {
|
|
2072
|
+
if (!secure) {
|
|
2073
|
+
throw new IQAuthError(
|
|
2074
|
+
"config_invalid",
|
|
2075
|
+
`Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
if (path !== "/") {
|
|
2079
|
+
throw new IQAuthError(
|
|
2080
|
+
"config_invalid",
|
|
2081
|
+
`Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
if (domain) {
|
|
2085
|
+
throw new IQAuthError(
|
|
2086
|
+
"config_invalid",
|
|
2087
|
+
`Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
} else if (name.startsWith("__Secure-") && !secure) {
|
|
2091
|
+
throw new IQAuthError(
|
|
2092
|
+
"config_invalid",
|
|
2093
|
+
`Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1865
2097
|
function resolve(config) {
|
|
1866
2098
|
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
1867
2099
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2100
|
+
maybeWarnDefaultSignoutRegistry(config);
|
|
2101
|
+
const secure = config.secure ?? true;
|
|
2102
|
+
if (config.secure === false && config.allowInsecureCookies !== true) {
|
|
2103
|
+
throw new IQAuthError(
|
|
2104
|
+
"config_invalid",
|
|
2105
|
+
"Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
|
|
2109
|
+
const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
|
|
2110
|
+
const stateCookieName = config.stateCookieName ?? "iqauth_state";
|
|
2111
|
+
const cookiePath = config.cookiePath ?? "/";
|
|
2112
|
+
const cookieDomain = config.cookieDomain;
|
|
2113
|
+
for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
|
|
2114
|
+
assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
|
|
2115
|
+
}
|
|
1868
2116
|
return {
|
|
1869
2117
|
publishableKey: config.publishableKey,
|
|
1870
2118
|
secretKey: config.secretKey,
|
|
1871
2119
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
1872
|
-
accessCookieName
|
|
1873
|
-
refreshCookieName
|
|
1874
|
-
cookieDomain
|
|
2120
|
+
accessCookieName,
|
|
2121
|
+
refreshCookieName,
|
|
2122
|
+
cookieDomain,
|
|
1875
2123
|
sameSite: config.sameSite ?? "lax",
|
|
1876
|
-
secure
|
|
1877
|
-
cookiePath
|
|
2124
|
+
secure,
|
|
2125
|
+
cookiePath,
|
|
1878
2126
|
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
1879
2127
|
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
1880
2128
|
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
@@ -1883,9 +2131,23 @@ function resolve(config) {
|
|
|
1883
2131
|
})),
|
|
1884
2132
|
appId: parsed.appId,
|
|
1885
2133
|
tenantId: parsed.tenantId,
|
|
1886
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2134
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2135
|
+
debug: config.debug,
|
|
2136
|
+
onTimingEvent: config.onTimingEvent,
|
|
2137
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2138
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2139
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2140
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
1887
2141
|
};
|
|
1888
2142
|
}
|
|
2143
|
+
function timingSafeEqualStr(a, b) {
|
|
2144
|
+
const len = Math.max(a.length, b.length);
|
|
2145
|
+
let diff = a.length ^ b.length;
|
|
2146
|
+
for (let i = 0; i < len; i++) {
|
|
2147
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
2148
|
+
}
|
|
2149
|
+
return diff === 0;
|
|
2150
|
+
}
|
|
1889
2151
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
1890
2152
|
return {
|
|
1891
2153
|
name,
|
|
@@ -1900,15 +2162,53 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
1900
2162
|
}
|
|
1901
2163
|
function clearCookies(cfg) {
|
|
1902
2164
|
return [
|
|
1903
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
1904
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2165
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2166
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
1905
2167
|
];
|
|
1906
2168
|
}
|
|
2169
|
+
function clearStateCookie(cfg) {
|
|
2170
|
+
return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
|
|
2171
|
+
}
|
|
2172
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2173
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2174
|
+
function pruneInMemoryMarkers(now) {
|
|
2175
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2176
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2177
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
var defaultSignoutRegistry = {
|
|
2181
|
+
mark(token, ttlMs) {
|
|
2182
|
+
const now = Date.now();
|
|
2183
|
+
pruneInMemoryMarkers(now);
|
|
2184
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2185
|
+
},
|
|
2186
|
+
has(token) {
|
|
2187
|
+
const now = Date.now();
|
|
2188
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2189
|
+
if (!exp) return false;
|
|
2190
|
+
if (exp <= now) {
|
|
2191
|
+
inMemorySignoutMarkers.delete(token);
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
return true;
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
var warnedDefaultSignoutRegistry = false;
|
|
2198
|
+
function maybeWarnDefaultSignoutRegistry(config) {
|
|
2199
|
+
if (warnedDefaultSignoutRegistry) return;
|
|
2200
|
+
if (config.signoutRegistry) return;
|
|
2201
|
+
warnedDefaultSignoutRegistry = true;
|
|
2202
|
+
console.warn(
|
|
2203
|
+
"[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
1907
2206
|
function serializeCookie(d) {
|
|
1908
2207
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
1909
2208
|
parts.push(`Path=${d.path}`);
|
|
1910
2209
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
1911
2210
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2211
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
1912
2212
|
if (d.secure) parts.push("Secure");
|
|
1913
2213
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
1914
2214
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -1916,14 +2216,34 @@ function serializeCookie(d) {
|
|
|
1916
2216
|
}
|
|
1917
2217
|
async function handleCallback(config, input) {
|
|
1918
2218
|
const cfg = resolve(config);
|
|
2219
|
+
const t0 = Date.now();
|
|
1919
2220
|
if (!input.code || !input.redirectUri) {
|
|
2221
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
1920
2222
|
return {
|
|
1921
2223
|
status: 400,
|
|
1922
2224
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
1923
2225
|
cookies: []
|
|
1924
2226
|
};
|
|
1925
2227
|
}
|
|
2228
|
+
const provided = input.state;
|
|
2229
|
+
const expected = input.expectedState;
|
|
2230
|
+
const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
|
|
2231
|
+
if (!stateOk) {
|
|
2232
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
|
|
2233
|
+
return {
|
|
2234
|
+
status: 400,
|
|
2235
|
+
body: {
|
|
2236
|
+
success: false,
|
|
2237
|
+
error: {
|
|
2238
|
+
code: "STATE_MISMATCH",
|
|
2239
|
+
message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
cookies: [clearStateCookie(cfg)]
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
1926
2245
|
if (!cfg.secretKey) {
|
|
2246
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
1927
2247
|
return {
|
|
1928
2248
|
status: 500,
|
|
1929
2249
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -1947,6 +2267,7 @@ async function handleCallback(config, input) {
|
|
|
1947
2267
|
});
|
|
1948
2268
|
const json = await res.json().catch(() => ({}));
|
|
1949
2269
|
if (!res.ok || !json.access_token) {
|
|
2270
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
1950
2271
|
return {
|
|
1951
2272
|
status: res.status || 502,
|
|
1952
2273
|
body: {
|
|
@@ -1959,6 +2280,26 @@ async function handleCallback(config, input) {
|
|
|
1959
2280
|
cookies: []
|
|
1960
2281
|
};
|
|
1961
2282
|
}
|
|
2283
|
+
try {
|
|
2284
|
+
await getTokensFor(cfg.issuer).verify(json.access_token, {
|
|
2285
|
+
issuer: cfg.issuer,
|
|
2286
|
+
...config.verify
|
|
2287
|
+
});
|
|
2288
|
+
} catch (err) {
|
|
2289
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2290
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
|
|
2291
|
+
return {
|
|
2292
|
+
status: 502,
|
|
2293
|
+
body: {
|
|
2294
|
+
success: false,
|
|
2295
|
+
error: {
|
|
2296
|
+
code: "ACCESS_TOKEN_VERIFICATION_FAILED",
|
|
2297
|
+
message: "The issuer returned an access token that failed verification; no session was established."
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
cookies: []
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
1962
2303
|
const cookies = [];
|
|
1963
2304
|
cookies.push(
|
|
1964
2305
|
makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
|
|
@@ -1966,6 +2307,8 @@ async function handleCallback(config, input) {
|
|
|
1966
2307
|
if (json.refresh_token) {
|
|
1967
2308
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
1968
2309
|
}
|
|
2310
|
+
cookies.push(clearStateCookie(cfg));
|
|
2311
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
1969
2312
|
return {
|
|
1970
2313
|
status: 200,
|
|
1971
2314
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -1974,8 +2317,18 @@ async function handleCallback(config, input) {
|
|
|
1974
2317
|
}
|
|
1975
2318
|
async function handleRefresh(config, input) {
|
|
1976
2319
|
const cfg = resolve(config);
|
|
2320
|
+
const t0 = Date.now();
|
|
1977
2321
|
const refreshToken = input.refreshToken;
|
|
2322
|
+
const idemKey = input.idempotencyToken;
|
|
2323
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2324
|
+
return {
|
|
2325
|
+
status: 401,
|
|
2326
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2327
|
+
cookies: clearCookies(cfg)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
1978
2330
|
if (!refreshToken) {
|
|
2331
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
1979
2332
|
return {
|
|
1980
2333
|
status: 401,
|
|
1981
2334
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -1991,6 +2344,7 @@ async function handleRefresh(config, input) {
|
|
|
1991
2344
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1992
2345
|
const status = res.status || 401;
|
|
1993
2346
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2347
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
1994
2348
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
1995
2349
|
cfg.clearCookiesOnRefreshFailure,
|
|
1996
2350
|
status,
|
|
@@ -2014,6 +2368,7 @@ async function handleRefresh(config, input) {
|
|
|
2014
2368
|
if (json.data.refreshToken) {
|
|
2015
2369
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2016
2370
|
}
|
|
2371
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2017
2372
|
return {
|
|
2018
2373
|
status: 200,
|
|
2019
2374
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2022,6 +2377,10 @@ async function handleRefresh(config, input) {
|
|
|
2022
2377
|
}
|
|
2023
2378
|
async function handleSignout(config, input) {
|
|
2024
2379
|
const cfg = resolve(config);
|
|
2380
|
+
const t0 = Date.now();
|
|
2381
|
+
if (input.idempotencyToken) {
|
|
2382
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2383
|
+
}
|
|
2025
2384
|
if (input.accessToken) {
|
|
2026
2385
|
try {
|
|
2027
2386
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2043,21 +2402,104 @@ async function handleSignout(config, input) {
|
|
|
2043
2402
|
} catch {
|
|
2044
2403
|
}
|
|
2045
2404
|
}
|
|
2405
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2046
2406
|
return {
|
|
2047
2407
|
status: 200,
|
|
2048
2408
|
body: { success: true, data: { signedOut: true } },
|
|
2049
2409
|
cookies: clearCookies(cfg)
|
|
2050
2410
|
};
|
|
2051
2411
|
}
|
|
2412
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2413
|
+
function getTokensFor(issuer) {
|
|
2414
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2415
|
+
if (!m) {
|
|
2416
|
+
m = new TokensModule(issuer);
|
|
2417
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2418
|
+
}
|
|
2419
|
+
return m;
|
|
2420
|
+
}
|
|
2421
|
+
async function handleUserinfo(config, input) {
|
|
2422
|
+
const cfg = resolve(config);
|
|
2423
|
+
if (!input.accessToken) {
|
|
2424
|
+
return {
|
|
2425
|
+
status: 401,
|
|
2426
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2427
|
+
cookies: []
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
let claims;
|
|
2431
|
+
try {
|
|
2432
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2433
|
+
issuer: cfg.issuer,
|
|
2434
|
+
...config.verify
|
|
2435
|
+
});
|
|
2436
|
+
} catch (err) {
|
|
2437
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2438
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2439
|
+
return {
|
|
2440
|
+
status: 401,
|
|
2441
|
+
body: { success: false, error: { code, message } },
|
|
2442
|
+
cookies: []
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2446
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2447
|
+
});
|
|
2448
|
+
return {
|
|
2449
|
+
status: 200,
|
|
2450
|
+
body: envelope,
|
|
2451
|
+
cookies: []
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// src/browser/returnTo.ts
|
|
2456
|
+
function normalizeOrigin(o) {
|
|
2457
|
+
try {
|
|
2458
|
+
return new URL(o).origin;
|
|
2459
|
+
} catch {
|
|
2460
|
+
return o.replace(/\/+$/, "");
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
function sanitizeReturnTo(input, options = {}) {
|
|
2464
|
+
const fallback = options.fallback ?? "/";
|
|
2465
|
+
if (!input || typeof input !== "string") return fallback;
|
|
2466
|
+
const trimmed = input.trim();
|
|
2467
|
+
if (!trimmed) return fallback;
|
|
2468
|
+
if (trimmed.includes("\\")) return fallback;
|
|
2469
|
+
if (trimmed.startsWith("//")) return fallback;
|
|
2470
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
2471
|
+
return trimmed;
|
|
2472
|
+
}
|
|
2473
|
+
if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
|
|
2474
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2475
|
+
}
|
|
2476
|
+
let parsed;
|
|
2477
|
+
try {
|
|
2478
|
+
parsed = new URL(trimmed);
|
|
2479
|
+
} catch {
|
|
2480
|
+
return fallback;
|
|
2481
|
+
}
|
|
2482
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
|
|
2483
|
+
const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
2484
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
2485
|
+
if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
|
|
2486
|
+
for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
|
|
2487
|
+
if (allowed.has(parsed.origin)) return parsed.toString();
|
|
2488
|
+
return fallback;
|
|
2489
|
+
}
|
|
2052
2490
|
|
|
2053
2491
|
// src/hono.ts
|
|
2492
|
+
var PKCE_COOKIE = "iqauth_pkce";
|
|
2054
2493
|
var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
2055
2494
|
"TOKEN_INVALID",
|
|
2056
2495
|
"TOKEN_EXPIRED",
|
|
2057
2496
|
"TOKEN_REVOKED",
|
|
2058
2497
|
"SESSION_EXPIRED",
|
|
2059
2498
|
"SESSION_INVALID",
|
|
2060
|
-
"AUTH_REQUIRED"
|
|
2499
|
+
"AUTH_REQUIRED",
|
|
2500
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy.
|
|
2501
|
+
"token_invalid",
|
|
2502
|
+
"token_expired"
|
|
2061
2503
|
]);
|
|
2062
2504
|
function readCookieFromHeader(header, name) {
|
|
2063
2505
|
if (!header) return void 0;
|
|
@@ -2079,6 +2521,36 @@ function honoResponse(hr) {
|
|
|
2079
2521
|
for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
|
|
2080
2522
|
return new Response(JSON.stringify(hr.body), { status: hr.status, headers });
|
|
2081
2523
|
}
|
|
2524
|
+
function honoCallbackResponse(hr, requestOrigin, returnToCookieValue, returnToCookieName) {
|
|
2525
|
+
const returnTo = sanitizeReturnTo(
|
|
2526
|
+
returnToCookieValue || hr.body?.returnTo,
|
|
2527
|
+
{ currentOrigin: requestOrigin, fallback: "/" }
|
|
2528
|
+
);
|
|
2529
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
2530
|
+
for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
|
|
2531
|
+
if (hr.status < 400) {
|
|
2532
|
+
headers.append("set-cookie", `${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2533
|
+
}
|
|
2534
|
+
const body = { ...hr.body, returnTo };
|
|
2535
|
+
return new Response(JSON.stringify(body), { status: hr.status, headers });
|
|
2536
|
+
}
|
|
2537
|
+
function honoCallbackRedirect(hr, requestOrigin, returnToCookieValue, cookieNames) {
|
|
2538
|
+
const headers = new Headers();
|
|
2539
|
+
for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
|
|
2540
|
+
headers.append("set-cookie", `${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2541
|
+
headers.append("set-cookie", `${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2542
|
+
if (hr.status >= 400) {
|
|
2543
|
+
headers.set("location", "/");
|
|
2544
|
+
return new Response(null, { status: 302, headers });
|
|
2545
|
+
}
|
|
2546
|
+
const dest = sanitizeReturnTo(returnToCookieValue, {
|
|
2547
|
+
currentOrigin: requestOrigin,
|
|
2548
|
+
fallback: "/"
|
|
2549
|
+
});
|
|
2550
|
+
headers.append("set-cookie", `${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2551
|
+
headers.set("location", dest);
|
|
2552
|
+
return new Response(null, { status: 302, headers });
|
|
2553
|
+
}
|
|
2082
2554
|
function iqAuth(options) {
|
|
2083
2555
|
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/hono" });
|
|
2084
2556
|
const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
@@ -2086,6 +2558,7 @@ function iqAuth(options) {
|
|
|
2086
2558
|
const client = new IQAuthClient({ baseUrl: issuer, environment: "server" });
|
|
2087
2559
|
const accessCookie = options.accessCookieName ?? "iqauth_at";
|
|
2088
2560
|
const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
|
|
2561
|
+
const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
|
|
2089
2562
|
const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
|
|
2090
2563
|
const mountHelpers = options.mountHelperRoutes !== false;
|
|
2091
2564
|
const isPublic = (p) => {
|
|
@@ -2096,24 +2569,52 @@ function iqAuth(options) {
|
|
|
2096
2569
|
return async (c, next) => {
|
|
2097
2570
|
const url = new URL(c.req.url);
|
|
2098
2571
|
const path = url.pathname;
|
|
2572
|
+
if (options.mountUserinfo && path === `${mount}/me` && c.req.method === "GET") {
|
|
2573
|
+
const auth2 = c.req.header("authorization");
|
|
2574
|
+
const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(c.req.header("cookie"), accessCookie);
|
|
2575
|
+
return honoResponse(await handleUserinfo(helperConfig, { accessToken, req: c.req }));
|
|
2576
|
+
}
|
|
2577
|
+
if (mountHelpers && path === `${mount}/callback` && c.req.method === "GET") {
|
|
2578
|
+
const cookieHeader = c.req.header("cookie");
|
|
2579
|
+
const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
|
|
2580
|
+
const hr = await handleCallback(helperConfig, {
|
|
2581
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
2582
|
+
codeVerifier: readCookieFromHeader(cookieHeader, PKCE_COOKIE),
|
|
2583
|
+
redirectUri: `${url.origin}${url.pathname}`,
|
|
2584
|
+
state: url.searchParams.get("state") ?? void 0,
|
|
2585
|
+
expectedState: readCookieFromHeader(cookieHeader, stateCookie)
|
|
2586
|
+
});
|
|
2587
|
+
return honoCallbackRedirect(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), {
|
|
2588
|
+
returnTo: returnToCookie,
|
|
2589
|
+
state: stateCookie,
|
|
2590
|
+
pkce: PKCE_COOKIE
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2099
2593
|
if (mountHelpers && path.startsWith(mount + "/") && c.req.method === "POST") {
|
|
2100
2594
|
const body = await c.req.json().catch(() => ({}));
|
|
2101
2595
|
const cookieHeader = c.req.header("cookie");
|
|
2102
2596
|
if (path === `${mount}/callback`) {
|
|
2103
|
-
|
|
2597
|
+
const hr = await handleCallback(helperConfig, {
|
|
2104
2598
|
code: body.code,
|
|
2105
2599
|
codeVerifier: body.codeVerifier,
|
|
2106
|
-
redirectUri: body.redirectUri
|
|
2107
|
-
|
|
2600
|
+
redirectUri: body.redirectUri,
|
|
2601
|
+
// M-2: bind callback to this browser; handleCallback fails closed.
|
|
2602
|
+
state: body.state,
|
|
2603
|
+
expectedState: readCookieFromHeader(cookieHeader, helperConfig.stateCookieName ?? "iqauth_state")
|
|
2604
|
+
});
|
|
2605
|
+
return honoCallbackResponse(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), returnToCookie);
|
|
2108
2606
|
}
|
|
2109
2607
|
if (path === `${mount}/refresh`) {
|
|
2110
2608
|
const refreshToken = body.refreshToken || readCookieFromHeader(cookieHeader, refreshCookie);
|
|
2111
|
-
|
|
2609
|
+
const idempotencyToken = c.req.header("x-iqauth-idempotency") || body.idempotencyToken;
|
|
2610
|
+
return honoResponse(await handleRefresh(helperConfig, { refreshToken, idempotencyToken }));
|
|
2112
2611
|
}
|
|
2113
2612
|
if (path === `${mount}/signout`) {
|
|
2114
2613
|
const auth2 = c.req.header("authorization");
|
|
2115
2614
|
const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(cookieHeader, accessCookie);
|
|
2116
|
-
|
|
2615
|
+
const refreshToken = readCookieFromHeader(cookieHeader, refreshCookie);
|
|
2616
|
+
const idempotencyToken = c.req.header("x-iqauth-idempotency");
|
|
2617
|
+
return honoResponse(await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader: cookieHeader }));
|
|
2117
2618
|
}
|
|
2118
2619
|
}
|
|
2119
2620
|
if (isPublic(path)) return next();
|