@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/server.js
CHANGED
|
@@ -35,25 +35,47 @@ __export(server_exports, {
|
|
|
35
35
|
ErrorCodes: () => ErrorCodes,
|
|
36
36
|
IQAuthClient: () => IQAuthClient,
|
|
37
37
|
IQAuthError: () => IQAuthError,
|
|
38
|
+
ProvisioningError: () => ProvisioningError,
|
|
38
39
|
ServerIQAuthClient: () => ServerIQAuthClient,
|
|
40
|
+
buildUserinfoResponse: () => buildUserinfoResponse,
|
|
41
|
+
createDrizzleLinkAdapter: () => createDrizzleLinkAdapter,
|
|
39
42
|
createProvisioningBridge: () => createProvisioningBridge,
|
|
40
43
|
createServerClient: () => createServerClient,
|
|
41
44
|
handleCallback: () => handleCallback,
|
|
42
45
|
handleRefresh: () => handleRefresh,
|
|
43
46
|
handleSignout: () => handleSignout,
|
|
47
|
+
handleUserinfo: () => handleUserinfo,
|
|
44
48
|
iqAuthMiddleware: () => iqAuthMiddleware,
|
|
49
|
+
linkLocalUserToIqAuthSub: () => linkLocalUserToIqAuthSub,
|
|
45
50
|
serializeCookie: () => serializeCookie
|
|
46
51
|
});
|
|
47
52
|
module.exports = __toCommonJS(server_exports);
|
|
48
53
|
|
|
49
54
|
// src/errors.ts
|
|
50
|
-
var IQAuthError = class extends Error {
|
|
51
|
-
constructor(code, message, status,
|
|
55
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
56
|
+
constructor(code, message, status, cause) {
|
|
52
57
|
super(message);
|
|
53
58
|
this.name = "IQAuthError";
|
|
54
59
|
this.code = code;
|
|
55
60
|
this.status = status;
|
|
56
|
-
this.
|
|
61
|
+
this.cause = cause;
|
|
62
|
+
this.raw = cause;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
66
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
67
|
+
*/
|
|
68
|
+
static isIQAuthError(value) {
|
|
69
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Type-narrowed code check. Lets callers write
|
|
73
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
74
|
+
* taxonomy without losing the ability to handle server codes via
|
|
75
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
76
|
+
*/
|
|
77
|
+
is(code) {
|
|
78
|
+
return this.code === code;
|
|
57
79
|
}
|
|
58
80
|
};
|
|
59
81
|
var ErrorCodes = {
|
|
@@ -204,7 +226,7 @@ var HttpClient = class {
|
|
|
204
226
|
headers: this.buildHeaders(),
|
|
205
227
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
206
228
|
const refreshToken = this.config.getRefreshToken();
|
|
207
|
-
if (!refreshToken) throw new IQAuthError("
|
|
229
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
208
230
|
return { body: JSON.stringify({ refreshToken }) };
|
|
209
231
|
})()
|
|
210
232
|
});
|
|
@@ -221,7 +243,7 @@ var HttpClient = class {
|
|
|
221
243
|
return;
|
|
222
244
|
}
|
|
223
245
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
224
|
-
throw new IQAuthError("
|
|
246
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
225
247
|
}
|
|
226
248
|
const tokens = {
|
|
227
249
|
accessToken: body.data.accessToken,
|
|
@@ -239,7 +261,7 @@ var HttpClient = class {
|
|
|
239
261
|
return this.requestWithRetry(method, path, body, options, false);
|
|
240
262
|
}
|
|
241
263
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
242
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
264
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
243
265
|
await this.attemptRefresh();
|
|
244
266
|
}
|
|
245
267
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -326,17 +348,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
326
348
|
tenants: data.tenants
|
|
327
349
|
};
|
|
328
350
|
}
|
|
351
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
352
|
+
return {
|
|
353
|
+
status: "scope_selection",
|
|
354
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
355
|
+
tenantId: data.tenantId,
|
|
356
|
+
scopes: data.scopes
|
|
357
|
+
};
|
|
358
|
+
}
|
|
329
359
|
throw new Error("Unexpected login response shape");
|
|
330
360
|
}
|
|
331
361
|
var AuthModule = class {
|
|
332
362
|
constructor(http) {
|
|
333
363
|
this.http = http;
|
|
334
364
|
}
|
|
335
|
-
async login(email, password) {
|
|
365
|
+
async login(email, password, opts) {
|
|
366
|
+
const body = { email, password };
|
|
367
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
336
368
|
const data = await this.http.request(
|
|
337
369
|
"POST",
|
|
338
370
|
"/api/v1/auth/login",
|
|
339
|
-
|
|
371
|
+
body,
|
|
340
372
|
{ skipAutoRefresh: true }
|
|
341
373
|
);
|
|
342
374
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -374,13 +406,29 @@ var AuthModule = class {
|
|
|
374
406
|
method
|
|
375
407
|
}, { skipAutoRefresh: true });
|
|
376
408
|
}
|
|
377
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
409
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
410
|
+
const body = { tenantSelectionToken, tenantId };
|
|
411
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
378
412
|
const data = await this.http.request(
|
|
379
413
|
"POST",
|
|
380
414
|
"/api/v1/auth/select-tenant",
|
|
415
|
+
body,
|
|
416
|
+
{ skipAutoRefresh: true }
|
|
417
|
+
);
|
|
418
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
422
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
423
|
+
* returned in the prior `scope_selection` envelope.
|
|
424
|
+
*/
|
|
425
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
426
|
+
const data = await this.http.request(
|
|
427
|
+
"POST",
|
|
428
|
+
"/api/v1/auth/select-scope",
|
|
381
429
|
{
|
|
382
|
-
|
|
383
|
-
|
|
430
|
+
scopeSelectionToken,
|
|
431
|
+
membershipId
|
|
384
432
|
},
|
|
385
433
|
{ skipAutoRefresh: true }
|
|
386
434
|
);
|
|
@@ -467,6 +515,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
467
515
|
"iqvalidate"
|
|
468
516
|
];
|
|
469
517
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
518
|
+
function classifyJoseError(err) {
|
|
519
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
520
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
521
|
+
}
|
|
522
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
523
|
+
return { code: "token_invalid", message: err.message };
|
|
524
|
+
}
|
|
525
|
+
if (err instanceof Error) {
|
|
526
|
+
return { code: "token_invalid", message: err.message };
|
|
527
|
+
}
|
|
528
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
529
|
+
}
|
|
470
530
|
function decodeProtectedHeader(token) {
|
|
471
531
|
const parts = token.split(".");
|
|
472
532
|
if (parts.length < 2) return null;
|
|
@@ -503,11 +563,11 @@ var TokensModule = class {
|
|
|
503
563
|
async verify(token, options = {}) {
|
|
504
564
|
const header = decodeProtectedHeader(token);
|
|
505
565
|
if (!header) {
|
|
506
|
-
throw new IQAuthError("
|
|
566
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
507
567
|
}
|
|
508
568
|
const kid = header.kid;
|
|
509
569
|
if (!kid) {
|
|
510
|
-
throw new IQAuthError("
|
|
570
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
511
571
|
}
|
|
512
572
|
let cache = await this.ensureCache();
|
|
513
573
|
if (!cache.byKid.has(kid)) {
|
|
@@ -515,7 +575,7 @@ var TokensModule = class {
|
|
|
515
575
|
cache = await this.ensureCache();
|
|
516
576
|
}
|
|
517
577
|
if (!cache.byKid.has(kid)) {
|
|
518
|
-
throw new IQAuthError("
|
|
578
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
519
579
|
}
|
|
520
580
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
521
581
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -531,16 +591,8 @@ var TokensModule = class {
|
|
|
531
591
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
532
592
|
return payload;
|
|
533
593
|
} catch (err) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
538
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
539
|
-
}
|
|
540
|
-
if (err instanceof Error) {
|
|
541
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
542
|
-
}
|
|
543
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
594
|
+
const classified = classifyJoseError(err);
|
|
595
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
544
596
|
}
|
|
545
597
|
}
|
|
546
598
|
/**
|
|
@@ -582,7 +634,7 @@ var TokensModule = class {
|
|
|
582
634
|
getClaims(token) {
|
|
583
635
|
const claims = this.decode(token);
|
|
584
636
|
if (!claims) {
|
|
585
|
-
throw new IQAuthError("
|
|
637
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
586
638
|
}
|
|
587
639
|
return claims;
|
|
588
640
|
}
|
|
@@ -592,7 +644,7 @@ var TokensModule = class {
|
|
|
592
644
|
}
|
|
593
645
|
await this.refreshJwks();
|
|
594
646
|
if (!this.jwksCache) {
|
|
595
|
-
throw new IQAuthError("
|
|
647
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
596
648
|
}
|
|
597
649
|
return this.jwksCache;
|
|
598
650
|
}
|
|
@@ -602,22 +654,38 @@ var TokensModule = class {
|
|
|
602
654
|
}
|
|
603
655
|
this.inFlightRefresh = (async () => {
|
|
604
656
|
try {
|
|
605
|
-
|
|
657
|
+
let res;
|
|
658
|
+
try {
|
|
659
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
throw new IQAuthError(
|
|
662
|
+
"network",
|
|
663
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
664
|
+
void 0,
|
|
665
|
+
err
|
|
666
|
+
);
|
|
667
|
+
}
|
|
606
668
|
if (!res.ok) {
|
|
607
669
|
throw new IQAuthError(
|
|
608
|
-
"
|
|
609
|
-
`Failed to fetch JWKS: ${res.status}
|
|
670
|
+
"jwks_fetch_failed",
|
|
671
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
672
|
+
res.status
|
|
610
673
|
);
|
|
611
674
|
}
|
|
612
675
|
let jwks;
|
|
613
676
|
try {
|
|
614
677
|
jwks = await res.json();
|
|
615
|
-
} catch {
|
|
616
|
-
throw new IQAuthError(
|
|
678
|
+
} catch (err) {
|
|
679
|
+
throw new IQAuthError(
|
|
680
|
+
"jwks_fetch_failed",
|
|
681
|
+
"Malformed JWKS response: invalid JSON",
|
|
682
|
+
res.status,
|
|
683
|
+
err
|
|
684
|
+
);
|
|
617
685
|
}
|
|
618
686
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
619
687
|
throw new IQAuthError(
|
|
620
|
-
"
|
|
688
|
+
"jwks_fetch_failed",
|
|
621
689
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
622
690
|
);
|
|
623
691
|
}
|
|
@@ -625,7 +693,7 @@ var TokensModule = class {
|
|
|
625
693
|
for (const key of jwks.keys) {
|
|
626
694
|
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")) {
|
|
627
695
|
throw new IQAuthError(
|
|
628
|
-
"
|
|
696
|
+
"jwks_fetch_failed",
|
|
629
697
|
"Malformed JWKS response: key missing required fields"
|
|
630
698
|
);
|
|
631
699
|
}
|
|
@@ -643,6 +711,19 @@ var TokensModule = class {
|
|
|
643
711
|
clearCache() {
|
|
644
712
|
this.jwksCache = null;
|
|
645
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
716
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
717
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
718
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
719
|
+
*/
|
|
720
|
+
async prewarm() {
|
|
721
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
722
|
+
try {
|
|
723
|
+
await this.refreshJwks();
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
}
|
|
646
727
|
};
|
|
647
728
|
|
|
648
729
|
// src/modules/sessions.ts
|
|
@@ -966,14 +1047,14 @@ var OidcModule = class {
|
|
|
966
1047
|
*/
|
|
967
1048
|
async handleCallback(params) {
|
|
968
1049
|
if (!params.state) {
|
|
969
|
-
throw new IQAuthError("
|
|
1050
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
970
1051
|
}
|
|
971
1052
|
if (!params.code) {
|
|
972
|
-
throw new IQAuthError("
|
|
1053
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
973
1054
|
}
|
|
974
1055
|
const stored = await this.stateStore.get(params.state);
|
|
975
1056
|
if (!stored) {
|
|
976
|
-
throw new IQAuthError("
|
|
1057
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
977
1058
|
}
|
|
978
1059
|
let tokens;
|
|
979
1060
|
try {
|
|
@@ -991,7 +1072,7 @@ var OidcModule = class {
|
|
|
991
1072
|
if (tokens.id_token) {
|
|
992
1073
|
if (!this.tokensModule) {
|
|
993
1074
|
throw new IQAuthError(
|
|
994
|
-
"
|
|
1075
|
+
"config_invalid",
|
|
995
1076
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
996
1077
|
);
|
|
997
1078
|
}
|
|
@@ -1002,7 +1083,7 @@ var OidcModule = class {
|
|
|
1002
1083
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
1003
1084
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
1004
1085
|
throw new IQAuthError(
|
|
1005
|
-
"
|
|
1086
|
+
"token_invalid",
|
|
1006
1087
|
"OIDC id_token nonce did not match the stored value"
|
|
1007
1088
|
);
|
|
1008
1089
|
}
|
|
@@ -1203,6 +1284,9 @@ var AppsModule = class {
|
|
|
1203
1284
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1204
1285
|
*/
|
|
1205
1286
|
async get(appKey) {
|
|
1287
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1288
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1289
|
+
}
|
|
1206
1290
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1207
1291
|
}
|
|
1208
1292
|
/**
|
|
@@ -1222,6 +1306,16 @@ var AppsModule = class {
|
|
|
1222
1306
|
401
|
|
1223
1307
|
);
|
|
1224
1308
|
}
|
|
1309
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1310
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1311
|
+
}
|
|
1312
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1313
|
+
throw new IQAuthError(
|
|
1314
|
+
"ENVIRONMENT_REQUIRED",
|
|
1315
|
+
"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.",
|
|
1316
|
+
400
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1225
1319
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1226
1320
|
}
|
|
1227
1321
|
/**
|
|
@@ -1231,11 +1325,14 @@ var AppsModule = class {
|
|
|
1231
1325
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1232
1326
|
*/
|
|
1233
1327
|
async isRegistered(appKey) {
|
|
1328
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1329
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1330
|
+
}
|
|
1234
1331
|
try {
|
|
1235
1332
|
await this.get(appKey);
|
|
1236
1333
|
return true;
|
|
1237
1334
|
} catch (err) {
|
|
1238
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1335
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1239
1336
|
return false;
|
|
1240
1337
|
}
|
|
1241
1338
|
throw err;
|
|
@@ -1272,6 +1369,20 @@ var RolesModule = class {
|
|
|
1272
1369
|
};
|
|
1273
1370
|
|
|
1274
1371
|
// src/modules/permissionGroups.ts
|
|
1372
|
+
function assertAppKey(appKey, callsite) {
|
|
1373
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1374
|
+
throw new IQAuthError(
|
|
1375
|
+
"VALIDATION_ERROR",
|
|
1376
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1377
|
+
400
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1382
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1383
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1275
1386
|
var PermissionGroupsModule = class {
|
|
1276
1387
|
constructor(http) {
|
|
1277
1388
|
this.http = http;
|
|
@@ -1292,7 +1403,14 @@ var PermissionGroupsModule = class {
|
|
|
1292
1403
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1293
1404
|
}
|
|
1294
1405
|
async addPermission(tenantId, groupId, data) {
|
|
1295
|
-
|
|
1406
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1407
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1408
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1409
|
+
appKey: data.appKey,
|
|
1410
|
+
nodeKey: data.nodeKey,
|
|
1411
|
+
effect: data.effect,
|
|
1412
|
+
weight: data.weight
|
|
1413
|
+
});
|
|
1296
1414
|
}
|
|
1297
1415
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1298
1416
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1316,21 +1434,51 @@ var PermissionGroupsModule = class {
|
|
|
1316
1434
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1317
1435
|
}
|
|
1318
1436
|
async addUserOverride(tenantId, userId, data) {
|
|
1319
|
-
|
|
1437
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1438
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1439
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1440
|
+
appKey: data.appKey,
|
|
1441
|
+
nodeKey: data.nodeKey,
|
|
1442
|
+
effect: data.effect,
|
|
1443
|
+
weight: data.weight,
|
|
1444
|
+
expiresAt: data.expiresAt
|
|
1445
|
+
});
|
|
1320
1446
|
}
|
|
1321
1447
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1322
1448
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1323
1449
|
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1452
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1453
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1454
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1455
|
+
*/
|
|
1324
1456
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const qs = query.toString();
|
|
1329
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1457
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1458
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1459
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1330
1460
|
}
|
|
1331
1461
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1462
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1463
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1332
1464
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1333
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1468
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1469
|
+
* single misconfigured entry can't slip through and silently report
|
|
1470
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1471
|
+
*/
|
|
1472
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1473
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1474
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1475
|
+
}
|
|
1476
|
+
checks.forEach((c, i) => {
|
|
1477
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1478
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1479
|
+
});
|
|
1480
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1481
|
+
}
|
|
1334
1482
|
};
|
|
1335
1483
|
|
|
1336
1484
|
// src/modules/apiKeys.ts
|
|
@@ -1755,6 +1903,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1755
1903
|
this._refreshToken = tokens.refreshToken;
|
|
1756
1904
|
},
|
|
1757
1905
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1906
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1907
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1908
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1909
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1758
1910
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1759
1911
|
sessionHeaderName: config.sessionHeaderName,
|
|
1760
1912
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1795,6 +1947,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1795
1947
|
static forServer(config) {
|
|
1796
1948
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1797
1949
|
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1952
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1953
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1954
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1955
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1956
|
+
*/
|
|
1798
1957
|
static forMobile(config) {
|
|
1799
1958
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1800
1959
|
}
|
|
@@ -1811,6 +1970,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1811
1970
|
getRefreshToken() {
|
|
1812
1971
|
return this._refreshToken;
|
|
1813
1972
|
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1975
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1976
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1977
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1978
|
+
*/
|
|
1979
|
+
async prewarm() {
|
|
1980
|
+
await Promise.all([
|
|
1981
|
+
this.tokens.prewarm(),
|
|
1982
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1983
|
+
]);
|
|
1984
|
+
}
|
|
1814
1985
|
getCurrentClaims() {
|
|
1815
1986
|
if (!this._accessToken) return null;
|
|
1816
1987
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1851,14 +2022,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1851
2022
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1852
2023
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1853
2024
|
throw new IQAuthError(
|
|
1854
|
-
"
|
|
2025
|
+
"config_invalid",
|
|
1855
2026
|
`${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.`
|
|
1856
2027
|
);
|
|
1857
2028
|
}
|
|
1858
2029
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1859
2030
|
if (!shapeMatch) {
|
|
1860
2031
|
throw new IQAuthError(
|
|
1861
|
-
"
|
|
2032
|
+
"config_invalid",
|
|
1862
2033
|
`${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.`
|
|
1863
2034
|
);
|
|
1864
2035
|
}
|
|
@@ -1867,19 +2038,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1867
2038
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1868
2039
|
} catch {
|
|
1869
2040
|
throw new IQAuthError(
|
|
1870
|
-
"
|
|
2041
|
+
"config_invalid",
|
|
1871
2042
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1872
2043
|
);
|
|
1873
2044
|
}
|
|
1874
2045
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1875
2046
|
throw new IQAuthError(
|
|
1876
|
-
"
|
|
2047
|
+
"config_invalid",
|
|
1877
2048
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1878
2049
|
);
|
|
1879
2050
|
}
|
|
1880
2051
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1881
2052
|
throw new IQAuthError(
|
|
1882
|
-
"
|
|
2053
|
+
"config_invalid",
|
|
1883
2054
|
`${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.`
|
|
1884
2055
|
);
|
|
1885
2056
|
}
|
|
@@ -1893,12 +2064,18 @@ function isPublishableKeyPayload(value) {
|
|
|
1893
2064
|
|
|
1894
2065
|
// src/middleware/express.ts
|
|
1895
2066
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2067
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1896
2068
|
"TOKEN_INVALID",
|
|
1897
2069
|
"TOKEN_EXPIRED",
|
|
1898
2070
|
"TOKEN_REVOKED",
|
|
1899
2071
|
"SESSION_EXPIRED",
|
|
1900
2072
|
"SESSION_INVALID",
|
|
1901
|
-
"AUTH_REQUIRED"
|
|
2073
|
+
"AUTH_REQUIRED",
|
|
2074
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2075
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2076
|
+
// codes to keep their auth-failure handling working.
|
|
2077
|
+
"token_invalid",
|
|
2078
|
+
"token_expired"
|
|
1902
2079
|
]);
|
|
1903
2080
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1904
2081
|
var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
|
|
@@ -2082,6 +2259,45 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2082
2259
|
}
|
|
2083
2260
|
|
|
2084
2261
|
// src/server/handlers.ts
|
|
2262
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2263
|
+
const baseUser = {
|
|
2264
|
+
sub: claims.sub,
|
|
2265
|
+
email: claims.email,
|
|
2266
|
+
name: claims.name,
|
|
2267
|
+
tenantId: claims.tenantId,
|
|
2268
|
+
vendorId: claims.vendorId,
|
|
2269
|
+
roles: claims.roles ?? [],
|
|
2270
|
+
entitlements: claims.entitlements ?? [],
|
|
2271
|
+
// Task #171 — project the active source/client scope onto the userinfo
|
|
2272
|
+
// payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
|
|
2273
|
+
// expose it without consumers having to re-decode the JWT.
|
|
2274
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
2275
|
+
};
|
|
2276
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2277
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2278
|
+
return {
|
|
2279
|
+
success: true,
|
|
2280
|
+
data: {
|
|
2281
|
+
user,
|
|
2282
|
+
claims,
|
|
2283
|
+
tenantId: claims.tenantId ?? null
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function emitTiming(cfg, event) {
|
|
2288
|
+
if (cfg.debug) {
|
|
2289
|
+
try {
|
|
2290
|
+
console.debug("[iqauth_helper]", event);
|
|
2291
|
+
} catch {
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (cfg.onTimingEvent) {
|
|
2295
|
+
try {
|
|
2296
|
+
cfg.onTimingEvent(event);
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2085
2301
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2086
2302
|
"TOKEN_REVOKED",
|
|
2087
2303
|
"SESSION_REVOKED",
|
|
@@ -2100,19 +2316,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
2100
2316
|
}
|
|
2101
2317
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2102
2318
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2319
|
+
function assertCookiePrefixInvariants(name, secure, path, domain) {
|
|
2320
|
+
if (name.startsWith("__Host-")) {
|
|
2321
|
+
if (!secure) {
|
|
2322
|
+
throw new IQAuthError(
|
|
2323
|
+
"config_invalid",
|
|
2324
|
+
`Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
if (path !== "/") {
|
|
2328
|
+
throw new IQAuthError(
|
|
2329
|
+
"config_invalid",
|
|
2330
|
+
`Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
if (domain) {
|
|
2334
|
+
throw new IQAuthError(
|
|
2335
|
+
"config_invalid",
|
|
2336
|
+
`Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
} else if (name.startsWith("__Secure-") && !secure) {
|
|
2340
|
+
throw new IQAuthError(
|
|
2341
|
+
"config_invalid",
|
|
2342
|
+
`Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2103
2346
|
function resolve(config) {
|
|
2104
2347
|
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2105
2348
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2349
|
+
maybeWarnDefaultSignoutRegistry(config);
|
|
2350
|
+
const secure = config.secure ?? true;
|
|
2351
|
+
if (config.secure === false && config.allowInsecureCookies !== true) {
|
|
2352
|
+
throw new IQAuthError(
|
|
2353
|
+
"config_invalid",
|
|
2354
|
+
"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."
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
|
|
2358
|
+
const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
|
|
2359
|
+
const stateCookieName = config.stateCookieName ?? "iqauth_state";
|
|
2360
|
+
const cookiePath = config.cookiePath ?? "/";
|
|
2361
|
+
const cookieDomain = config.cookieDomain;
|
|
2362
|
+
for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
|
|
2363
|
+
assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
|
|
2364
|
+
}
|
|
2106
2365
|
return {
|
|
2107
2366
|
publishableKey: config.publishableKey,
|
|
2108
2367
|
secretKey: config.secretKey,
|
|
2109
2368
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2110
|
-
accessCookieName
|
|
2111
|
-
refreshCookieName
|
|
2112
|
-
cookieDomain
|
|
2369
|
+
accessCookieName,
|
|
2370
|
+
refreshCookieName,
|
|
2371
|
+
cookieDomain,
|
|
2113
2372
|
sameSite: config.sameSite ?? "lax",
|
|
2114
|
-
secure
|
|
2115
|
-
cookiePath
|
|
2373
|
+
secure,
|
|
2374
|
+
cookiePath,
|
|
2116
2375
|
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
2117
2376
|
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
2118
2377
|
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
@@ -2121,9 +2380,23 @@ function resolve(config) {
|
|
|
2121
2380
|
})),
|
|
2122
2381
|
appId: parsed.appId,
|
|
2123
2382
|
tenantId: parsed.tenantId,
|
|
2124
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2383
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2384
|
+
debug: config.debug,
|
|
2385
|
+
onTimingEvent: config.onTimingEvent,
|
|
2386
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2387
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2388
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2389
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
2125
2390
|
};
|
|
2126
2391
|
}
|
|
2392
|
+
function timingSafeEqualStr(a, b) {
|
|
2393
|
+
const len = Math.max(a.length, b.length);
|
|
2394
|
+
let diff = a.length ^ b.length;
|
|
2395
|
+
for (let i = 0; i < len; i++) {
|
|
2396
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
2397
|
+
}
|
|
2398
|
+
return diff === 0;
|
|
2399
|
+
}
|
|
2127
2400
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
2128
2401
|
return {
|
|
2129
2402
|
name,
|
|
@@ -2138,15 +2411,53 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
2138
2411
|
}
|
|
2139
2412
|
function clearCookies(cfg) {
|
|
2140
2413
|
return [
|
|
2141
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
2142
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2414
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2415
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
2143
2416
|
];
|
|
2144
2417
|
}
|
|
2418
|
+
function clearStateCookie(cfg) {
|
|
2419
|
+
return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
|
|
2420
|
+
}
|
|
2421
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2422
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2423
|
+
function pruneInMemoryMarkers(now) {
|
|
2424
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2425
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2426
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
var defaultSignoutRegistry = {
|
|
2430
|
+
mark(token, ttlMs) {
|
|
2431
|
+
const now = Date.now();
|
|
2432
|
+
pruneInMemoryMarkers(now);
|
|
2433
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2434
|
+
},
|
|
2435
|
+
has(token) {
|
|
2436
|
+
const now = Date.now();
|
|
2437
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2438
|
+
if (!exp) return false;
|
|
2439
|
+
if (exp <= now) {
|
|
2440
|
+
inMemorySignoutMarkers.delete(token);
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
var warnedDefaultSignoutRegistry = false;
|
|
2447
|
+
function maybeWarnDefaultSignoutRegistry(config) {
|
|
2448
|
+
if (warnedDefaultSignoutRegistry) return;
|
|
2449
|
+
if (config.signoutRegistry) return;
|
|
2450
|
+
warnedDefaultSignoutRegistry = true;
|
|
2451
|
+
console.warn(
|
|
2452
|
+
"[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."
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2145
2455
|
function serializeCookie(d) {
|
|
2146
2456
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
2147
2457
|
parts.push(`Path=${d.path}`);
|
|
2148
2458
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
2149
2459
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2460
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
2150
2461
|
if (d.secure) parts.push("Secure");
|
|
2151
2462
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
2152
2463
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -2154,14 +2465,34 @@ function serializeCookie(d) {
|
|
|
2154
2465
|
}
|
|
2155
2466
|
async function handleCallback(config, input) {
|
|
2156
2467
|
const cfg = resolve(config);
|
|
2468
|
+
const t0 = Date.now();
|
|
2157
2469
|
if (!input.code || !input.redirectUri) {
|
|
2470
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
2158
2471
|
return {
|
|
2159
2472
|
status: 400,
|
|
2160
2473
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
2161
2474
|
cookies: []
|
|
2162
2475
|
};
|
|
2163
2476
|
}
|
|
2477
|
+
const provided = input.state;
|
|
2478
|
+
const expected = input.expectedState;
|
|
2479
|
+
const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
|
|
2480
|
+
if (!stateOk) {
|
|
2481
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
|
|
2482
|
+
return {
|
|
2483
|
+
status: 400,
|
|
2484
|
+
body: {
|
|
2485
|
+
success: false,
|
|
2486
|
+
error: {
|
|
2487
|
+
code: "STATE_MISMATCH",
|
|
2488
|
+
message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
|
|
2489
|
+
}
|
|
2490
|
+
},
|
|
2491
|
+
cookies: [clearStateCookie(cfg)]
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2164
2494
|
if (!cfg.secretKey) {
|
|
2495
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
2165
2496
|
return {
|
|
2166
2497
|
status: 500,
|
|
2167
2498
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -2185,6 +2516,7 @@ async function handleCallback(config, input) {
|
|
|
2185
2516
|
});
|
|
2186
2517
|
const json = await res.json().catch(() => ({}));
|
|
2187
2518
|
if (!res.ok || !json.access_token) {
|
|
2519
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
2188
2520
|
return {
|
|
2189
2521
|
status: res.status || 502,
|
|
2190
2522
|
body: {
|
|
@@ -2197,6 +2529,26 @@ async function handleCallback(config, input) {
|
|
|
2197
2529
|
cookies: []
|
|
2198
2530
|
};
|
|
2199
2531
|
}
|
|
2532
|
+
try {
|
|
2533
|
+
await getTokensFor(cfg.issuer).verify(json.access_token, {
|
|
2534
|
+
issuer: cfg.issuer,
|
|
2535
|
+
...config.verify
|
|
2536
|
+
});
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2539
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
|
|
2540
|
+
return {
|
|
2541
|
+
status: 502,
|
|
2542
|
+
body: {
|
|
2543
|
+
success: false,
|
|
2544
|
+
error: {
|
|
2545
|
+
code: "ACCESS_TOKEN_VERIFICATION_FAILED",
|
|
2546
|
+
message: "The issuer returned an access token that failed verification; no session was established."
|
|
2547
|
+
}
|
|
2548
|
+
},
|
|
2549
|
+
cookies: []
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2200
2552
|
const cookies = [];
|
|
2201
2553
|
cookies.push(
|
|
2202
2554
|
makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
|
|
@@ -2204,6 +2556,8 @@ async function handleCallback(config, input) {
|
|
|
2204
2556
|
if (json.refresh_token) {
|
|
2205
2557
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
2206
2558
|
}
|
|
2559
|
+
cookies.push(clearStateCookie(cfg));
|
|
2560
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
2207
2561
|
return {
|
|
2208
2562
|
status: 200,
|
|
2209
2563
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -2212,8 +2566,18 @@ async function handleCallback(config, input) {
|
|
|
2212
2566
|
}
|
|
2213
2567
|
async function handleRefresh(config, input) {
|
|
2214
2568
|
const cfg = resolve(config);
|
|
2569
|
+
const t0 = Date.now();
|
|
2215
2570
|
const refreshToken = input.refreshToken;
|
|
2571
|
+
const idemKey = input.idempotencyToken;
|
|
2572
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2573
|
+
return {
|
|
2574
|
+
status: 401,
|
|
2575
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2576
|
+
cookies: clearCookies(cfg)
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2216
2579
|
if (!refreshToken) {
|
|
2580
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
2217
2581
|
return {
|
|
2218
2582
|
status: 401,
|
|
2219
2583
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -2229,6 +2593,7 @@ async function handleRefresh(config, input) {
|
|
|
2229
2593
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2230
2594
|
const status = res.status || 401;
|
|
2231
2595
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2596
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
2232
2597
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
2233
2598
|
cfg.clearCookiesOnRefreshFailure,
|
|
2234
2599
|
status,
|
|
@@ -2252,6 +2617,7 @@ async function handleRefresh(config, input) {
|
|
|
2252
2617
|
if (json.data.refreshToken) {
|
|
2253
2618
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2254
2619
|
}
|
|
2620
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2255
2621
|
return {
|
|
2256
2622
|
status: 200,
|
|
2257
2623
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2260,6 +2626,10 @@ async function handleRefresh(config, input) {
|
|
|
2260
2626
|
}
|
|
2261
2627
|
async function handleSignout(config, input) {
|
|
2262
2628
|
const cfg = resolve(config);
|
|
2629
|
+
const t0 = Date.now();
|
|
2630
|
+
if (input.idempotencyToken) {
|
|
2631
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2632
|
+
}
|
|
2263
2633
|
if (input.accessToken) {
|
|
2264
2634
|
try {
|
|
2265
2635
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2281,14 +2651,64 @@ async function handleSignout(config, input) {
|
|
|
2281
2651
|
} catch {
|
|
2282
2652
|
}
|
|
2283
2653
|
}
|
|
2654
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2284
2655
|
return {
|
|
2285
2656
|
status: 200,
|
|
2286
2657
|
body: { success: true, data: { signedOut: true } },
|
|
2287
2658
|
cookies: clearCookies(cfg)
|
|
2288
2659
|
};
|
|
2289
2660
|
}
|
|
2661
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2662
|
+
function getTokensFor(issuer) {
|
|
2663
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2664
|
+
if (!m) {
|
|
2665
|
+
m = new TokensModule(issuer);
|
|
2666
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2667
|
+
}
|
|
2668
|
+
return m;
|
|
2669
|
+
}
|
|
2670
|
+
async function handleUserinfo(config, input) {
|
|
2671
|
+
const cfg = resolve(config);
|
|
2672
|
+
if (!input.accessToken) {
|
|
2673
|
+
return {
|
|
2674
|
+
status: 401,
|
|
2675
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2676
|
+
cookies: []
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
let claims;
|
|
2680
|
+
try {
|
|
2681
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2682
|
+
issuer: cfg.issuer,
|
|
2683
|
+
...config.verify
|
|
2684
|
+
});
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2687
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2688
|
+
return {
|
|
2689
|
+
status: 401,
|
|
2690
|
+
body: { success: false, error: { code, message } },
|
|
2691
|
+
cookies: []
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2695
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2696
|
+
});
|
|
2697
|
+
return {
|
|
2698
|
+
status: 200,
|
|
2699
|
+
body: envelope,
|
|
2700
|
+
cookies: []
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2290
2703
|
|
|
2291
2704
|
// src/server/provisioningBridge.ts
|
|
2705
|
+
var ProvisioningError = class extends Error {
|
|
2706
|
+
constructor(code, message) {
|
|
2707
|
+
super(message);
|
|
2708
|
+
this.name = "ProvisioningError";
|
|
2709
|
+
this.code = code;
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2292
2712
|
function defaultIsUniqueViolation(err) {
|
|
2293
2713
|
if (!err || typeof err !== "object") return false;
|
|
2294
2714
|
const e = err;
|
|
@@ -2300,6 +2720,16 @@ function defaultIsUniqueViolation(err) {
|
|
|
2300
2720
|
function createProvisioningBridge(options) {
|
|
2301
2721
|
const { storage } = options;
|
|
2302
2722
|
const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
|
|
2723
|
+
const allowUnverifiedEmailAdopt = options.allowUnverifiedEmailAdopt === true;
|
|
2724
|
+
const emailVerified = (claims) => claims.email_verified === true;
|
|
2725
|
+
const assertAdoptAllowed = (claims) => {
|
|
2726
|
+
if (!allowUnverifiedEmailAdopt && !emailVerified(claims)) {
|
|
2727
|
+
throw new ProvisioningError(
|
|
2728
|
+
"UNVERIFIED_EMAIL_ADOPT_REFUSED",
|
|
2729
|
+
"Refusing to adopt a pre-existing local account from an unverified email (claims.email_verified !== true). Set allowUnverifiedEmailAdopt:true only if your issuer is trusted to never emit unverified emails for adoption."
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2303
2733
|
const roleOf = (claims) => {
|
|
2304
2734
|
try {
|
|
2305
2735
|
return options.roleMapper?.(claims) ?? null;
|
|
@@ -2316,6 +2746,7 @@ function createProvisioningBridge(options) {
|
|
|
2316
2746
|
if (claims.email) {
|
|
2317
2747
|
const byEmail = await storage.findByEmail(claims.email);
|
|
2318
2748
|
if (byEmail) {
|
|
2749
|
+
assertAdoptAllowed(claims);
|
|
2319
2750
|
if (storage.adoptByEmail) {
|
|
2320
2751
|
const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
|
|
2321
2752
|
return { user: adopted, claims, created: false, adopted: true };
|
|
@@ -2331,7 +2762,10 @@ function createProvisioningBridge(options) {
|
|
|
2331
2762
|
if (after) return { user: after, claims, created: false, adopted: false };
|
|
2332
2763
|
if (claims.email) {
|
|
2333
2764
|
const byEmail = await storage.findByEmail(claims.email);
|
|
2334
|
-
if (byEmail)
|
|
2765
|
+
if (byEmail) {
|
|
2766
|
+
assertAdoptAllowed(claims);
|
|
2767
|
+
return { user: byEmail, claims, created: false, adopted: true };
|
|
2768
|
+
}
|
|
2335
2769
|
}
|
|
2336
2770
|
throw err;
|
|
2337
2771
|
}
|
|
@@ -2339,6 +2773,85 @@ function createProvisioningBridge(options) {
|
|
|
2339
2773
|
return { ensureUser };
|
|
2340
2774
|
}
|
|
2341
2775
|
|
|
2776
|
+
// src/server/linkLocalUser.ts
|
|
2777
|
+
async function linkLocalUserToIqAuthSub(options) {
|
|
2778
|
+
const { adapter, claims } = options;
|
|
2779
|
+
const lookupBy = options.lookupBy ?? ["email"];
|
|
2780
|
+
const caseInsensitive = options.caseInsensitiveEmail !== false;
|
|
2781
|
+
if (!claims?.sub) throw new Error("linkLocalUserToIqAuthSub: claims.sub is required");
|
|
2782
|
+
return adapter.withTransaction(async (tx) => {
|
|
2783
|
+
const bySub = await tx.findByIqAuthSub(claims.sub);
|
|
2784
|
+
if (bySub) return { status: "already_linked", userId: bySub.id };
|
|
2785
|
+
for (const key of lookupBy) {
|
|
2786
|
+
if (key !== "email") continue;
|
|
2787
|
+
if (!claims.email) continue;
|
|
2788
|
+
const matches = await tx.findByEmail(claims.email, { caseInsensitive });
|
|
2789
|
+
if (matches.length === 0) continue;
|
|
2790
|
+
if (matches.length > 1) {
|
|
2791
|
+
return { status: "conflict", reason: "duplicate_email" };
|
|
2792
|
+
}
|
|
2793
|
+
const row = matches[0];
|
|
2794
|
+
if (row.iqauthSub && row.iqauthSub !== claims.sub) {
|
|
2795
|
+
return { status: "conflict", userId: row.id, reason: "different_sub" };
|
|
2796
|
+
}
|
|
2797
|
+
if (row.iqauthSub === claims.sub) {
|
|
2798
|
+
return { status: "already_linked", userId: row.id };
|
|
2799
|
+
}
|
|
2800
|
+
if (claims.email_verified !== true && options.allowUnverifiedEmail !== true) {
|
|
2801
|
+
return { status: "conflict", userId: row.id, reason: "unverified_email" };
|
|
2802
|
+
}
|
|
2803
|
+
const wrote = await tx.setIqAuthSub(row.id, claims.sub);
|
|
2804
|
+
if (wrote === false) {
|
|
2805
|
+
return { status: "conflict", userId: row.id, reason: "different_sub" };
|
|
2806
|
+
}
|
|
2807
|
+
return { status: "linked", userId: row.id };
|
|
2808
|
+
}
|
|
2809
|
+
return { status: "not_found" };
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
function createDrizzleLinkAdapter(deps) {
|
|
2813
|
+
const { db, table, columns, eq, sql } = deps;
|
|
2814
|
+
const iqauthSubKey = deps.columnNames?.iqauthSub ?? "iqauthSub";
|
|
2815
|
+
return {
|
|
2816
|
+
async withTransaction(fn) {
|
|
2817
|
+
return db.transaction(async (txDb) => {
|
|
2818
|
+
const lockedRead = async (cond, limit) => {
|
|
2819
|
+
const built = txDb.select().from(table).where(cond).limit(limit);
|
|
2820
|
+
if (typeof built.for === "function") {
|
|
2821
|
+
return built.for("update");
|
|
2822
|
+
}
|
|
2823
|
+
return built;
|
|
2824
|
+
};
|
|
2825
|
+
const tx = {
|
|
2826
|
+
async findByIqAuthSub(sub) {
|
|
2827
|
+
const rows = await lockedRead(eq(columns.iqauthSub, sub), 1);
|
|
2828
|
+
return rows[0] ?? null;
|
|
2829
|
+
},
|
|
2830
|
+
async findByEmail(email, { caseInsensitive }) {
|
|
2831
|
+
const cond = caseInsensitive ? sql`lower(${columns.email}) = lower(${email})` : eq(columns.email, email);
|
|
2832
|
+
return lockedRead(cond, 2);
|
|
2833
|
+
},
|
|
2834
|
+
async setIqAuthSub(userId, sub) {
|
|
2835
|
+
const result = await txDb.update(table).set({ [iqauthSubKey]: sub }).where(
|
|
2836
|
+
sql`${columns.id} = ${userId} AND (${columns.iqauthSub} IS NULL OR ${columns.iqauthSub} = ${sub})`
|
|
2837
|
+
);
|
|
2838
|
+
const r = result;
|
|
2839
|
+
if (Array.isArray(r)) {
|
|
2840
|
+
return (r[0]?.affectedRows ?? 1) > 0;
|
|
2841
|
+
}
|
|
2842
|
+
if (r && typeof r === "object") {
|
|
2843
|
+
const n = r.rowCount ?? r.rowsAffected ?? r.changes;
|
|
2844
|
+
if (typeof n === "number") return n > 0;
|
|
2845
|
+
}
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
return fn(tx);
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2342
2855
|
// src/server.ts
|
|
2343
2856
|
var ServerIQAuthClient = class extends IQAuthClient {
|
|
2344
2857
|
constructor(config) {
|
|
@@ -2358,12 +2871,17 @@ function createServerClient(config) {
|
|
|
2358
2871
|
ErrorCodes,
|
|
2359
2872
|
IQAuthClient,
|
|
2360
2873
|
IQAuthError,
|
|
2874
|
+
ProvisioningError,
|
|
2361
2875
|
ServerIQAuthClient,
|
|
2876
|
+
buildUserinfoResponse,
|
|
2877
|
+
createDrizzleLinkAdapter,
|
|
2362
2878
|
createProvisioningBridge,
|
|
2363
2879
|
createServerClient,
|
|
2364
2880
|
handleCallback,
|
|
2365
2881
|
handleRefresh,
|
|
2366
2882
|
handleSignout,
|
|
2883
|
+
handleUserinfo,
|
|
2367
2884
|
iqAuthMiddleware,
|
|
2885
|
+
linkLocalUserToIqAuthSub,
|
|
2368
2886
|
serializeCookie
|
|
2369
2887
|
});
|