@iqauth/sdk 2.2.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/dist/browser-session.d.mts +3 -3
- package/dist/browser-session.d.ts +3 -3
- package/dist/browser-session.js +89 -68
- package/dist/browser-session.mjs +2 -1
- package/dist/browser.d.mts +64 -29
- package/dist/browser.d.ts +64 -29
- package/dist/browser.js +794 -39
- package/dist/browser.mjs +44 -4
- package/dist/bundle-LUKDQYVQ.mjs +374 -0
- package/dist/chunk-3JULWS6F.mjs +106 -0
- package/dist/chunk-5T7GHBX6.mjs +1165 -0
- package/dist/{chunk-M4J6BPK7.mjs → chunk-6TDJJER7.mjs} +12 -3
- package/dist/{chunk-QZB745C2.mjs → chunk-76W5TLQQ.mjs} +264 -211
- package/dist/{chunk-D72UL5HL.mjs → chunk-BVV54LPI.mjs} +36 -4
- package/dist/chunk-LIZYFXH7.mjs +90 -0
- package/dist/chunk-MKKZULZR.mjs +241 -0
- package/dist/chunk-SL3KRS4W.mjs +54 -0
- package/dist/chunk-TKZTCPEK.mjs +232 -0
- package/dist/chunk-UKZLOHZG.mjs +83 -0
- package/dist/chunk-UNYDG2L4.mjs +209 -0
- package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
- package/dist/{chunk-QEJB7WEQ.mjs → chunk-WQWBJSSS.mjs} +1 -1
- package/dist/cli/index.js +144 -36
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DXbHb2ul.d.ts → client-BNQe3AgF.d.ts} +3 -67
- package/dist/{client-Dv4v92Mj.d.mts → client-kYlJFgPv.d.mts} +3 -67
- package/dist/doctor-YYNHNMLD.mjs +198 -0
- package/dist/{express-BZmF1llh.d.mts → express-B6_1vBYZ.d.mts} +23 -2
- package/dist/{express-B4o3P8vK.d.ts → express-CHpfa7D_.d.ts} +23 -2
- package/dist/express.d.mts +77 -6
- package/dist/express.d.ts +77 -6
- package/dist/express.js +336 -74
- package/dist/express.mjs +209 -8
- package/dist/fastify.js +103 -72
- package/dist/fastify.mjs +6 -4
- package/dist/hono.js +102 -72
- package/dist/hono.mjs +5 -4
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +590 -73
- package/dist/index.mjs +30 -8
- package/dist/locales.d.mts +53 -0
- package/dist/locales.d.ts +53 -0
- package/dist/locales.js +1202 -0
- package/dist/locales.mjs +29 -0
- package/dist/mobile.d.mts +3 -3
- package/dist/mobile.d.ts +3 -3
- package/dist/mobile.js +89 -68
- package/dist/mobile.mjs +2 -1
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +101 -1618
- package/dist/next.mjs +9 -9
- package/dist/provisioningBridge-88xjOS2n.d.mts +86 -0
- package/dist/provisioningBridge-DnTfzdZK.d.ts +86 -0
- package/dist/react.d.mts +1349 -10
- package/dist/react.d.ts +1349 -10
- package/dist/react.js +2998 -569
- package/dist/react.mjs +1518 -95
- package/dist/reverify-4UEJXUS6.mjs +16 -0
- package/dist/server/handlers.d.mts +12 -1
- package/dist/server/handlers.d.ts +12 -1
- package/dist/server/handlers.js +12 -3
- package/dist/server/handlers.mjs +2 -2
- package/dist/server.d.mts +5 -4
- package/dist/server.d.ts +5 -4
- package/dist/server.js +188 -73
- package/dist/server.mjs +13 -8
- package/dist/service.d.mts +3 -3
- package/dist/service.d.ts +3 -3
- package/dist/service.js +89 -68
- package/dist/service.mjs +2 -1
- package/dist/signIn-CCY4JE5G.mjs +15 -0
- package/dist/{signIn-D_kP3v-c.d.mts → signIn-CiIBTJIh.d.mts} +232 -4
- package/dist/{signIn-BVDTIA_t.d.ts → signIn-OCr88Zf8.d.ts} +232 -4
- package/dist/test.d.mts +86 -0
- package/dist/test.d.ts +86 -0
- package/dist/test.js +289 -0
- package/dist/test.mjs +9 -0
- package/dist/tokens-DCyzzn8L.d.mts +63 -0
- package/dist/tokens-aHiGFr_E.d.ts +63 -0
- package/dist/types-6bNdxesb.d.mts +196 -0
- package/dist/types-6bNdxesb.d.ts +196 -0
- package/dist/{types-Cxl3bQHt.d.ts → types-DZAflmmq.d.mts} +6 -0
- package/dist/{types-Cxl3bQHt.d.mts → types-DZAflmmq.d.ts} +6 -0
- package/dist/webhooks.d.mts +61 -0
- package/dist/webhooks.d.ts +61 -0
- package/dist/webhooks.js +119 -0
- package/dist/webhooks.mjs +11 -0
- package/dist/ws.d.mts +73 -0
- package/dist/ws.d.ts +73 -0
- package/dist/ws.js +397 -0
- package/dist/ws.mjs +12 -0
- package/package.json +24 -3
- package/dist/doctor-XCI77BQS.mjs +0 -90
package/dist/index.js
CHANGED
|
@@ -60,13 +60,19 @@ __export(index_exports, {
|
|
|
60
60
|
TokensModule: () => TokensModule,
|
|
61
61
|
UsersModule: () => UsersModule,
|
|
62
62
|
VendorsModule: () => VendorsModule,
|
|
63
|
+
WebhookSignatureError: () => WebhookSignatureError,
|
|
63
64
|
WebhooksModule: () => WebhooksModule,
|
|
64
65
|
assertPublishableKey: () => assertPublishableKey,
|
|
66
|
+
createProvisioningBridge: () => createProvisioningBridge,
|
|
67
|
+
createTestIssuer: () => createTestIssuer,
|
|
65
68
|
encodePublishableKey: () => encodePublishableKey,
|
|
66
69
|
iqAuthMiddleware: () => iqAuthMiddleware,
|
|
67
70
|
isPublishableKey: () => isPublishableKey,
|
|
68
71
|
isSecretKey: () => isSecretKey,
|
|
69
|
-
|
|
72
|
+
isValidWebhookSignature: () => isValidWebhookSignature,
|
|
73
|
+
parsePublishableKey: () => parsePublishableKey,
|
|
74
|
+
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
75
|
+
verifyWsUpgrade: () => verifyWsUpgrade
|
|
70
76
|
});
|
|
71
77
|
module.exports = __toCommonJS(index_exports);
|
|
72
78
|
|
|
@@ -478,8 +484,7 @@ function parseMfaResponse(data, browserSessionMode) {
|
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
// src/modules/tokens.ts
|
|
481
|
-
var
|
|
482
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
487
|
+
var import_jose = require("jose");
|
|
483
488
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
484
489
|
var DEFAULT_TOKEN_ISSUER = [
|
|
485
490
|
"https://auth.dispositioniq.com",
|
|
@@ -492,6 +497,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
492
497
|
"iqvalidate"
|
|
493
498
|
];
|
|
494
499
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
500
|
+
function decodeProtectedHeader(token) {
|
|
501
|
+
const parts = token.split(".");
|
|
502
|
+
if (parts.length < 2) return null;
|
|
503
|
+
try {
|
|
504
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
505
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
506
|
+
let json;
|
|
507
|
+
if (typeof atob === "function") {
|
|
508
|
+
json = atob(b64);
|
|
509
|
+
} else {
|
|
510
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
511
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
512
|
+
}
|
|
513
|
+
return JSON.parse(json);
|
|
514
|
+
} catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
495
518
|
var TokensModule = class {
|
|
496
519
|
constructor(baseUrl, options = {}) {
|
|
497
520
|
this.jwksCache = null;
|
|
@@ -502,49 +525,49 @@ var TokensModule = class {
|
|
|
502
525
|
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
503
526
|
}
|
|
504
527
|
/**
|
|
505
|
-
* Verify a JWT access token using RS256 via JWKS from
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* clock tolerance default to client config but can be overridden per call.
|
|
528
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
529
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
530
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
531
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
510
532
|
*/
|
|
511
533
|
async verify(token, options = {}) {
|
|
512
|
-
const
|
|
513
|
-
if (!
|
|
534
|
+
const header = decodeProtectedHeader(token);
|
|
535
|
+
if (!header) {
|
|
514
536
|
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
515
537
|
}
|
|
516
|
-
const kid =
|
|
538
|
+
const kid = header.kid;
|
|
517
539
|
if (!kid) {
|
|
518
540
|
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
519
541
|
}
|
|
520
|
-
let
|
|
521
|
-
if (!
|
|
522
|
-
|
|
523
|
-
|
|
542
|
+
let cache = await this.ensureCache();
|
|
543
|
+
if (!cache.byKid.has(kid)) {
|
|
544
|
+
this.jwksCache = null;
|
|
545
|
+
cache = await this.ensureCache();
|
|
524
546
|
}
|
|
525
|
-
if (!
|
|
547
|
+
if (!cache.byKid.has(kid)) {
|
|
526
548
|
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
527
549
|
}
|
|
528
550
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
529
551
|
const audience = options.audience ?? this.defaultAudience;
|
|
530
552
|
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
531
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
553
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
554
|
+
const verifyOptions = {
|
|
555
|
+
algorithms,
|
|
556
|
+
clockTolerance,
|
|
557
|
+
issuer,
|
|
558
|
+
audience
|
|
559
|
+
};
|
|
532
560
|
try {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
clockTolerance,
|
|
536
|
-
// The jsonwebtoken types insist on tuple types for arrays; runtime
|
|
537
|
-
// accepts plain string[] so we cast to satisfy the compiler.
|
|
538
|
-
issuer,
|
|
539
|
-
audience
|
|
540
|
-
};
|
|
541
|
-
const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
|
|
542
|
-
return verified;
|
|
561
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
562
|
+
return payload;
|
|
543
563
|
} catch (err) {
|
|
564
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
565
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
566
|
+
}
|
|
567
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
568
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
569
|
+
}
|
|
544
570
|
if (err instanceof Error) {
|
|
545
|
-
if (err.name === "TokenExpiredError") {
|
|
546
|
-
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
547
|
-
}
|
|
548
571
|
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
549
572
|
}
|
|
550
573
|
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
@@ -552,29 +575,40 @@ var TokensModule = class {
|
|
|
552
575
|
}
|
|
553
576
|
/**
|
|
554
577
|
* Decode a JWT without verification. Returns null if malformed.
|
|
555
|
-
*
|
|
556
|
-
* @remarks Local decode only — no network call
|
|
557
578
|
*/
|
|
558
579
|
decode(token) {
|
|
559
|
-
|
|
560
|
-
|
|
580
|
+
try {
|
|
581
|
+
const parts = token.split(".");
|
|
582
|
+
if (parts.length < 2) return null;
|
|
583
|
+
const payload = parts[1];
|
|
584
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
585
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
586
|
+
let json;
|
|
587
|
+
if (typeof atob === "function") {
|
|
588
|
+
json = atob(b64);
|
|
589
|
+
} else {
|
|
590
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
591
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
json = decodeURIComponent(escape(json));
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
const claims = JSON.parse(json);
|
|
598
|
+
if (!claims || typeof claims !== "object") return null;
|
|
599
|
+
return claims;
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
561
603
|
}
|
|
562
|
-
/**
|
|
563
|
-
* Check if a token is expired based on the `exp` claim.
|
|
564
|
-
*
|
|
565
|
-
* @remarks Local check only — no network call
|
|
566
|
-
*/
|
|
604
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
567
605
|
isExpired(token) {
|
|
568
606
|
const claims = this.decode(token);
|
|
569
607
|
if (!claims?.exp) return true;
|
|
570
608
|
const now = Math.floor(Date.now() / 1e3);
|
|
571
609
|
return claims.exp <= now;
|
|
572
610
|
}
|
|
573
|
-
/**
|
|
574
|
-
* Get the claims from a token without verification.
|
|
575
|
-
*
|
|
576
|
-
* @remarks Local decode only — no network call
|
|
577
|
-
*/
|
|
611
|
+
/** Get the claims from a token without verification. */
|
|
578
612
|
getClaims(token) {
|
|
579
613
|
const claims = this.decode(token);
|
|
580
614
|
if (!claims) {
|
|
@@ -582,11 +616,15 @@ var TokensModule = class {
|
|
|
582
616
|
}
|
|
583
617
|
return claims;
|
|
584
618
|
}
|
|
585
|
-
async
|
|
586
|
-
if (
|
|
587
|
-
|
|
619
|
+
async ensureCache() {
|
|
620
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
621
|
+
return this.jwksCache;
|
|
622
|
+
}
|
|
623
|
+
await this.refreshJwks();
|
|
624
|
+
if (!this.jwksCache) {
|
|
625
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
588
626
|
}
|
|
589
|
-
return this.jwksCache
|
|
627
|
+
return this.jwksCache;
|
|
590
628
|
}
|
|
591
629
|
async refreshJwks() {
|
|
592
630
|
if (this.inFlightRefresh) {
|
|
@@ -613,35 +651,24 @@ var TokensModule = class {
|
|
|
613
651
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
614
652
|
);
|
|
615
653
|
}
|
|
616
|
-
const
|
|
654
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
617
655
|
for (const key of jwks.keys) {
|
|
618
|
-
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
|
|
656
|
+
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")) {
|
|
619
657
|
throw new IQAuthError(
|
|
620
658
|
"INTERNAL_ERROR",
|
|
621
659
|
"Malformed JWKS response: key missing required fields"
|
|
622
660
|
);
|
|
623
661
|
}
|
|
624
|
-
|
|
625
|
-
keys.set(key.kid, pem);
|
|
662
|
+
byKid.add(key.kid);
|
|
626
663
|
}
|
|
627
|
-
|
|
664
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
665
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
628
666
|
} finally {
|
|
629
667
|
this.inFlightRefresh = null;
|
|
630
668
|
}
|
|
631
669
|
})();
|
|
632
670
|
return this.inFlightRefresh;
|
|
633
671
|
}
|
|
634
|
-
jwkToPem(jwk) {
|
|
635
|
-
const keyObject = import_crypto.default.createPublicKey({
|
|
636
|
-
key: {
|
|
637
|
-
kty: jwk.kty,
|
|
638
|
-
n: jwk.n,
|
|
639
|
-
e: jwk.e
|
|
640
|
-
},
|
|
641
|
-
format: "jwk"
|
|
642
|
-
});
|
|
643
|
-
return keyObject.export({ type: "spki", format: "pem" });
|
|
644
|
-
}
|
|
645
672
|
/** @internal Exposed for testing — clears JWKS cache */
|
|
646
673
|
clearCache() {
|
|
647
674
|
this.jwksCache = null;
|
|
@@ -849,7 +876,7 @@ var PermissionsModule = class {
|
|
|
849
876
|
};
|
|
850
877
|
|
|
851
878
|
// src/modules/oidc.ts
|
|
852
|
-
var
|
|
879
|
+
var import_crypto = __toESM(require("crypto"));
|
|
853
880
|
var InMemoryOidcStateStore = class {
|
|
854
881
|
constructor() {
|
|
855
882
|
this.map = /* @__PURE__ */ new Map();
|
|
@@ -930,12 +957,12 @@ var OidcModule = class {
|
|
|
930
957
|
* ready to redirect the user to.
|
|
931
958
|
*/
|
|
932
959
|
async createAuthRequest(params) {
|
|
933
|
-
const codeVerifier = base64UrlEncode(
|
|
960
|
+
const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
|
|
934
961
|
const codeChallenge = base64UrlEncode(
|
|
935
|
-
|
|
962
|
+
import_crypto.default.createHash("sha256").update(codeVerifier).digest()
|
|
936
963
|
);
|
|
937
|
-
const state = base64UrlEncode(
|
|
938
|
-
const nonce = base64UrlEncode(
|
|
964
|
+
const state = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
965
|
+
const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
939
966
|
await this.stateStore.set(state, {
|
|
940
967
|
codeVerifier,
|
|
941
968
|
state,
|
|
@@ -1913,7 +1940,7 @@ function assertPublishableKey(raw, opts) {
|
|
|
1913
1940
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1914
1941
|
throw new IQAuthError(
|
|
1915
1942
|
"CONFIG_INVALID",
|
|
1916
|
-
`${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
|
|
1943
|
+
`${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.`
|
|
1917
1944
|
);
|
|
1918
1945
|
}
|
|
1919
1946
|
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
@@ -1964,6 +1991,22 @@ function readCookie(req, name) {
|
|
|
1964
1991
|
}
|
|
1965
1992
|
return void 0;
|
|
1966
1993
|
}
|
|
1994
|
+
function compileMatcher(pat) {
|
|
1995
|
+
if (pat instanceof RegExp) return (p) => pat.test(p);
|
|
1996
|
+
const re = new RegExp(
|
|
1997
|
+
"^" + pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLE::").replace(/\*/g, "[^/]*").replace(/::DOUBLE::/g, ".*") + "$"
|
|
1998
|
+
);
|
|
1999
|
+
return (p) => re.test(p);
|
|
2000
|
+
}
|
|
2001
|
+
function compileMatchers(pats) {
|
|
2002
|
+
return (pats ?? []).map(compileMatcher);
|
|
2003
|
+
}
|
|
2004
|
+
function pathOf(req) {
|
|
2005
|
+
const r = req;
|
|
2006
|
+
const raw = r.path || r.originalUrl || r.url || "/";
|
|
2007
|
+
const q = raw.indexOf("?");
|
|
2008
|
+
return q >= 0 ? raw.slice(0, q) : raw;
|
|
2009
|
+
}
|
|
1967
2010
|
function clientFromPublishableKey(opts) {
|
|
1968
2011
|
const parsed = assertPublishableKey(opts.publishableKey, { context: "iqAuthMiddleware" });
|
|
1969
2012
|
const issuer = (opts.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
@@ -1991,10 +2034,26 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
1991
2034
|
onUnauthorized,
|
|
1992
2035
|
onForbidden,
|
|
1993
2036
|
onError,
|
|
1994
|
-
|
|
1995
|
-
|
|
2037
|
+
cookieAware = true,
|
|
2038
|
+
cookieNames,
|
|
2039
|
+
protect,
|
|
2040
|
+
publicRoutes
|
|
1996
2041
|
} = resolvedOptions;
|
|
2042
|
+
const accessCookieName = resolvedOptions.accessCookieName ?? cookieNames?.access ?? DEFAULT_ACCESS_COOKIE;
|
|
2043
|
+
const protectMatchers = compileMatchers(protect);
|
|
2044
|
+
const publicMatchers = compileMatchers(publicRoutes);
|
|
2045
|
+
const hasProtect = protectMatchers.length > 0;
|
|
2046
|
+
const hasPublic = publicMatchers.length > 0;
|
|
1997
2047
|
return async (req, res, next) => {
|
|
2048
|
+
if (hasProtect || hasPublic) {
|
|
2049
|
+
const path = pathOf(req);
|
|
2050
|
+
if (hasPublic && publicMatchers.some((m) => m(path))) {
|
|
2051
|
+
return next();
|
|
2052
|
+
}
|
|
2053
|
+
if (hasProtect && !protectMatchers.some((m) => m(path))) {
|
|
2054
|
+
return next();
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1998
2057
|
let token;
|
|
1999
2058
|
const authHeader = getAuthorizationHeader(req);
|
|
2000
2059
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
@@ -2086,6 +2145,458 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2086
2145
|
next();
|
|
2087
2146
|
};
|
|
2088
2147
|
}
|
|
2148
|
+
|
|
2149
|
+
// src/ws.ts
|
|
2150
|
+
var DEFAULT_COOKIE = "iqauth_at";
|
|
2151
|
+
var DEFAULT_SUBPROTOCOL_PREFIX = "iqauth.bearer.";
|
|
2152
|
+
var JWT_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
2153
|
+
var tokensByIssuer = /* @__PURE__ */ new Map();
|
|
2154
|
+
function getTokens(issuer) {
|
|
2155
|
+
let mod = tokensByIssuer.get(issuer);
|
|
2156
|
+
if (!mod) {
|
|
2157
|
+
mod = new TokensModule(issuer);
|
|
2158
|
+
tokensByIssuer.set(issuer, mod);
|
|
2159
|
+
}
|
|
2160
|
+
return mod;
|
|
2161
|
+
}
|
|
2162
|
+
function firstHeader(value) {
|
|
2163
|
+
if (Array.isArray(value)) return value[0];
|
|
2164
|
+
return value;
|
|
2165
|
+
}
|
|
2166
|
+
function readCookie2(cookieHeader, name) {
|
|
2167
|
+
if (!cookieHeader) return void 0;
|
|
2168
|
+
const target = `${name}=`;
|
|
2169
|
+
for (const seg of cookieHeader.split(";")) {
|
|
2170
|
+
const t = seg.trim();
|
|
2171
|
+
if (t.startsWith(target)) {
|
|
2172
|
+
try {
|
|
2173
|
+
return decodeURIComponent(t.slice(target.length));
|
|
2174
|
+
} catch {
|
|
2175
|
+
return t.slice(target.length);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
return void 0;
|
|
2180
|
+
}
|
|
2181
|
+
function extractToken(req, cookieName, subprotocolPrefix) {
|
|
2182
|
+
let authHeader;
|
|
2183
|
+
let cookieHeader;
|
|
2184
|
+
let subprotoHeader;
|
|
2185
|
+
if ("headers" in req && req.headers && typeof req.headers === "object") {
|
|
2186
|
+
authHeader = firstHeader(req.headers.authorization);
|
|
2187
|
+
cookieHeader = firstHeader(req.headers.cookie);
|
|
2188
|
+
subprotoHeader = firstHeader(req.headers["sec-websocket-protocol"]);
|
|
2189
|
+
} else {
|
|
2190
|
+
const r = req;
|
|
2191
|
+
authHeader = r.authorization;
|
|
2192
|
+
cookieHeader = r.cookie;
|
|
2193
|
+
subprotoHeader = r.secWebSocketProtocol;
|
|
2194
|
+
}
|
|
2195
|
+
if (authHeader && /^Bearer /i.test(authHeader)) {
|
|
2196
|
+
return authHeader.slice(7).trim();
|
|
2197
|
+
}
|
|
2198
|
+
if (cookieName && cookieHeader) {
|
|
2199
|
+
const fromCookie = readCookie2(cookieHeader, cookieName);
|
|
2200
|
+
if (fromCookie) return fromCookie;
|
|
2201
|
+
}
|
|
2202
|
+
if (subprotocolPrefix !== null && subprotoHeader) {
|
|
2203
|
+
const protos = subprotoHeader.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2204
|
+
for (const proto of protos) {
|
|
2205
|
+
if (subprotocolPrefix && proto.startsWith(subprotocolPrefix)) {
|
|
2206
|
+
return proto.slice(subprotocolPrefix.length);
|
|
2207
|
+
}
|
|
2208
|
+
if (JWT_SHAPE.test(proto)) return proto;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return void 0;
|
|
2212
|
+
}
|
|
2213
|
+
async function verifyWsUpgrade(req, options) {
|
|
2214
|
+
const parsed = assertPublishableKey(options.publishableKey, {
|
|
2215
|
+
context: "@iqauth/sdk/ws"
|
|
2216
|
+
});
|
|
2217
|
+
const issuer = (options.issuer && typeof options.issuer === "string" ? options.issuer : parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`).replace(/\/+$/, "");
|
|
2218
|
+
const cookieName = options.cookieName === void 0 ? DEFAULT_COOKIE : options.cookieName;
|
|
2219
|
+
const subprotocolPrefix = options.subprotocolPrefix === void 0 ? DEFAULT_SUBPROTOCOL_PREFIX : options.subprotocolPrefix;
|
|
2220
|
+
const token = extractToken(req, cookieName, subprotocolPrefix);
|
|
2221
|
+
if (!token) return null;
|
|
2222
|
+
const tokens = getTokens(issuer);
|
|
2223
|
+
try {
|
|
2224
|
+
const verifyOpts = {};
|
|
2225
|
+
if (options.audience !== void 0) verifyOpts.audience = options.audience;
|
|
2226
|
+
verifyOpts.issuer = options.issuer ?? issuer;
|
|
2227
|
+
if (options.clockTolerance !== void 0)
|
|
2228
|
+
verifyOpts.clockTolerance = options.clockTolerance;
|
|
2229
|
+
if (options.algorithms !== void 0) verifyOpts.algorithms = options.algorithms;
|
|
2230
|
+
const claims = await tokens.verify(token, verifyOpts);
|
|
2231
|
+
return { claims };
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
if (err instanceof IQAuthError) return null;
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/test.ts
|
|
2239
|
+
var import_http2 = require("http");
|
|
2240
|
+
var import_crypto2 = require("crypto");
|
|
2241
|
+
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
2242
|
+
function jwkFromPublicKey(publicKey, kid) {
|
|
2243
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
2244
|
+
return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
|
|
2245
|
+
}
|
|
2246
|
+
function readBody(req) {
|
|
2247
|
+
return new Promise((resolve, reject) => {
|
|
2248
|
+
const chunks = [];
|
|
2249
|
+
req.on("data", (c) => chunks.push(c));
|
|
2250
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
2251
|
+
req.on("error", reject);
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
function parseFormOrJson(raw, contentType) {
|
|
2255
|
+
if (!raw) return {};
|
|
2256
|
+
if (contentType && contentType.includes("application/json")) {
|
|
2257
|
+
try {
|
|
2258
|
+
const obj = JSON.parse(raw);
|
|
2259
|
+
const out2 = {};
|
|
2260
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
2261
|
+
if (typeof v === "string") out2[k] = v;
|
|
2262
|
+
else if (v != null) out2[k] = String(v);
|
|
2263
|
+
}
|
|
2264
|
+
return out2;
|
|
2265
|
+
} catch {
|
|
2266
|
+
return {};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
const out = {};
|
|
2270
|
+
for (const part of raw.split("&")) {
|
|
2271
|
+
if (!part) continue;
|
|
2272
|
+
const eq = part.indexOf("=");
|
|
2273
|
+
const k = decodeURIComponent(eq === -1 ? part : part.slice(0, eq)).replace(/\+/g, " ");
|
|
2274
|
+
const v = eq === -1 ? "" : decodeURIComponent(part.slice(eq + 1)).replace(/\+/g, " ");
|
|
2275
|
+
out[k] = v;
|
|
2276
|
+
}
|
|
2277
|
+
return out;
|
|
2278
|
+
}
|
|
2279
|
+
function send(res, status, body, headers = {}) {
|
|
2280
|
+
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
|
2281
|
+
res.writeHead(status, {
|
|
2282
|
+
"Content-Type": typeof body === "string" ? "text/plain; charset=utf-8" : "application/json; charset=utf-8",
|
|
2283
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
2284
|
+
...headers
|
|
2285
|
+
});
|
|
2286
|
+
res.end(payload);
|
|
2287
|
+
}
|
|
2288
|
+
async function createTestIssuer(options = {}) {
|
|
2289
|
+
const host = options.host ?? "127.0.0.1";
|
|
2290
|
+
const port = options.port ?? 0;
|
|
2291
|
+
const tenantId = options.tenantId ?? "tenant-test";
|
|
2292
|
+
const appId = options.appId ?? "app-test";
|
|
2293
|
+
const kid = options.kid ?? `test-${(0, import_crypto2.randomBytes)(6).toString("hex")}`;
|
|
2294
|
+
const defaultAudience = options.defaultAudience ?? ["dispositioniq"];
|
|
2295
|
+
const { privateKey, publicKey } = (0, import_crypto2.generateKeyPairSync)("rsa", {
|
|
2296
|
+
modulusLength: 2048,
|
|
2297
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
2298
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
2299
|
+
});
|
|
2300
|
+
const publicKeyObj = (0, import_crypto2.createPublicKey)(publicKey);
|
|
2301
|
+
const jwk = jwkFromPublicKey(publicKeyObj, kid);
|
|
2302
|
+
const pendingCodes = /* @__PURE__ */ new Map();
|
|
2303
|
+
let baseUrl = "";
|
|
2304
|
+
const buildToken = (opts) => {
|
|
2305
|
+
const payload = {
|
|
2306
|
+
sub: opts.sub ?? "test-user",
|
|
2307
|
+
email: opts.email ?? "test@example.com",
|
|
2308
|
+
name: opts.name ?? "Test User",
|
|
2309
|
+
tenantId: opts.tenantId ?? tenantId,
|
|
2310
|
+
vendorId: opts.vendorId ?? null,
|
|
2311
|
+
roles: opts.roles ?? [],
|
|
2312
|
+
entitlements: opts.entitlements ?? [],
|
|
2313
|
+
sessionId: opts.sessionId ?? `sess-${(0, import_crypto2.randomBytes)(4).toString("hex")}`,
|
|
2314
|
+
jti: opts.jti ?? `jti-${(0, import_crypto2.randomBytes)(4).toString("hex")}`
|
|
2315
|
+
};
|
|
2316
|
+
if (opts.scopeContext !== void 0) payload.scopeContext = opts.scopeContext;
|
|
2317
|
+
if (opts.loginMethod !== void 0) payload.loginMethod = opts.loginMethod;
|
|
2318
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
2319
|
+
if (["sub", "email", "name", "tenantId", "vendorId", "roles", "entitlements", "sessionId", "jti", "scopeContext", "loginMethod", "audience", "issuer", "expiresInSeconds", "iat"].includes(k))
|
|
2320
|
+
continue;
|
|
2321
|
+
payload[k] = v;
|
|
2322
|
+
}
|
|
2323
|
+
const audience = opts.audience ?? defaultAudience;
|
|
2324
|
+
const issuer = opts.issuer ?? baseUrl;
|
|
2325
|
+
const expiresIn = opts.expiresInSeconds ?? 900;
|
|
2326
|
+
const signOpts = {
|
|
2327
|
+
algorithm: "RS256",
|
|
2328
|
+
keyid: kid,
|
|
2329
|
+
issuer,
|
|
2330
|
+
audience
|
|
2331
|
+
};
|
|
2332
|
+
if (opts.iat !== void 0) {
|
|
2333
|
+
payload.iat = opts.iat;
|
|
2334
|
+
payload.exp = opts.iat + expiresIn;
|
|
2335
|
+
} else {
|
|
2336
|
+
signOpts.expiresIn = expiresIn;
|
|
2337
|
+
}
|
|
2338
|
+
return import_jsonwebtoken.default.sign(payload, privateKey, signOpts);
|
|
2339
|
+
};
|
|
2340
|
+
const handler = async (req, res) => {
|
|
2341
|
+
try {
|
|
2342
|
+
const url = new URL(req.url || "/", baseUrl || `http://${host}`);
|
|
2343
|
+
const path = url.pathname;
|
|
2344
|
+
if (req.method === "OPTIONS") {
|
|
2345
|
+
res.writeHead(204, {
|
|
2346
|
+
"Access-Control-Allow-Origin": "*",
|
|
2347
|
+
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
|
2348
|
+
"Access-Control-Allow-Headers": "Authorization,Content-Type"
|
|
2349
|
+
});
|
|
2350
|
+
return res.end();
|
|
2351
|
+
}
|
|
2352
|
+
const cors = { "Access-Control-Allow-Origin": "*" };
|
|
2353
|
+
if (req.method === "GET" && path === "/.well-known/openid-configuration") {
|
|
2354
|
+
return send(res, 200, {
|
|
2355
|
+
issuer: baseUrl,
|
|
2356
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
2357
|
+
authorization_endpoint: `${baseUrl}/oidc/authorize`,
|
|
2358
|
+
token_endpoint: `${baseUrl}/oidc/token`,
|
|
2359
|
+
userinfo_endpoint: `${baseUrl}/api/v1/auth/me`,
|
|
2360
|
+
response_types_supported: ["code"],
|
|
2361
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2362
|
+
subject_types_supported: ["public"],
|
|
2363
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
2364
|
+
code_challenge_methods_supported: ["S256"]
|
|
2365
|
+
}, cors);
|
|
2366
|
+
}
|
|
2367
|
+
if (req.method === "GET" && path === "/.well-known/jwks.json") {
|
|
2368
|
+
return send(res, 200, { keys: [jwk] }, { ...cors, "Cache-Control": "public, max-age=3600" });
|
|
2369
|
+
}
|
|
2370
|
+
if (req.method === "POST" && path === "/oidc/token") {
|
|
2371
|
+
const raw = await readBody(req);
|
|
2372
|
+
const params = parseFormOrJson(raw, req.headers["content-type"]);
|
|
2373
|
+
const grant = params.grant_type;
|
|
2374
|
+
if (grant === "authorization_code") {
|
|
2375
|
+
const code = params.code;
|
|
2376
|
+
const pending = code ? pendingCodes.get(code) : void 0;
|
|
2377
|
+
if (!pending) {
|
|
2378
|
+
return send(res, 400, { error: "invalid_grant", error_description: "Unknown or expired code" }, cors);
|
|
2379
|
+
}
|
|
2380
|
+
pendingCodes.delete(code);
|
|
2381
|
+
const accessToken = buildToken(pending.claims);
|
|
2382
|
+
return send(res, 200, {
|
|
2383
|
+
access_token: accessToken,
|
|
2384
|
+
refresh_token: pending.refreshToken,
|
|
2385
|
+
id_token: accessToken,
|
|
2386
|
+
token_type: "Bearer",
|
|
2387
|
+
expires_in: pending.claims.expiresInSeconds ?? 900
|
|
2388
|
+
}, cors);
|
|
2389
|
+
}
|
|
2390
|
+
if (grant === "refresh_token") {
|
|
2391
|
+
const accessToken = buildToken({ sub: "test-user" });
|
|
2392
|
+
return send(res, 200, {
|
|
2393
|
+
access_token: accessToken,
|
|
2394
|
+
refresh_token: params.refresh_token || `rt-${(0, import_crypto2.randomBytes)(8).toString("hex")}`,
|
|
2395
|
+
token_type: "Bearer",
|
|
2396
|
+
expires_in: 900
|
|
2397
|
+
}, cors);
|
|
2398
|
+
}
|
|
2399
|
+
return send(res, 400, { error: "unsupported_grant_type" }, cors);
|
|
2400
|
+
}
|
|
2401
|
+
if (req.method === "GET" && path === "/api/v1/auth/me") {
|
|
2402
|
+
const auth = req.headers.authorization || "";
|
|
2403
|
+
if (!/^Bearer /i.test(auth)) {
|
|
2404
|
+
return send(res, 401, { success: false, error: { code: "TOKEN_INVALID", message: "Missing bearer" } }, cors);
|
|
2405
|
+
}
|
|
2406
|
+
const token = auth.slice(7).trim();
|
|
2407
|
+
try {
|
|
2408
|
+
const decoded = import_jsonwebtoken.default.verify(token, publicKey, {
|
|
2409
|
+
algorithms: ["RS256"],
|
|
2410
|
+
issuer: baseUrl,
|
|
2411
|
+
audience: defaultAudience
|
|
2412
|
+
});
|
|
2413
|
+
return send(res, 200, {
|
|
2414
|
+
success: true,
|
|
2415
|
+
data: {
|
|
2416
|
+
id: decoded.sub,
|
|
2417
|
+
email: decoded.email,
|
|
2418
|
+
name: decoded.name,
|
|
2419
|
+
tenantId: decoded.tenantId,
|
|
2420
|
+
roles: decoded.roles,
|
|
2421
|
+
entitlements: decoded.entitlements
|
|
2422
|
+
}
|
|
2423
|
+
}, cors);
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
const msg = err instanceof Error ? err.message : "verify failed";
|
|
2426
|
+
return send(res, 401, { success: false, error: { code: "TOKEN_INVALID", message: msg } }, cors);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
send(res, 404, { error: "not_found", path }, cors);
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
const msg = err instanceof Error ? err.message : "internal error";
|
|
2432
|
+
send(res, 500, { error: "internal", message: msg });
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
const server = (0, import_http2.createServer)((req, res) => {
|
|
2436
|
+
void handler(req, res);
|
|
2437
|
+
});
|
|
2438
|
+
await new Promise((resolve, reject) => {
|
|
2439
|
+
server.once("error", reject);
|
|
2440
|
+
server.listen(port, host, () => {
|
|
2441
|
+
server.off("error", reject);
|
|
2442
|
+
resolve();
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
const addr = server.address();
|
|
2446
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : port;
|
|
2447
|
+
baseUrl = `http://${host}:${boundPort}`;
|
|
2448
|
+
const publishableKey = encodePublishableKey("test", {
|
|
2449
|
+
iss: baseUrl,
|
|
2450
|
+
appId,
|
|
2451
|
+
tenantId,
|
|
2452
|
+
kid
|
|
2453
|
+
});
|
|
2454
|
+
return {
|
|
2455
|
+
baseUrl,
|
|
2456
|
+
publishableKey,
|
|
2457
|
+
kid,
|
|
2458
|
+
publicKey,
|
|
2459
|
+
mintToken: (opts = {}) => buildToken(opts),
|
|
2460
|
+
mintAuthCode: (opts = {}) => {
|
|
2461
|
+
const code = `code-${(0, import_crypto2.randomBytes)(12).toString("hex")}`;
|
|
2462
|
+
const refreshToken = opts.refreshToken ?? `rt-${(0, import_crypto2.randomBytes)(12).toString("hex")}`;
|
|
2463
|
+
pendingCodes.set(code, { claims: opts, refreshToken });
|
|
2464
|
+
return code;
|
|
2465
|
+
},
|
|
2466
|
+
close: () => new Promise((resolve, reject) => {
|
|
2467
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
2468
|
+
})
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/webhooks.ts
|
|
2473
|
+
var import_crypto3 = __toESM(require("crypto"));
|
|
2474
|
+
var WebhookSignatureError = class extends Error {
|
|
2475
|
+
constructor(code, message) {
|
|
2476
|
+
super(message);
|
|
2477
|
+
this.name = "WebhookSignatureError";
|
|
2478
|
+
this.code = code;
|
|
2479
|
+
}
|
|
2480
|
+
};
|
|
2481
|
+
function toBuffer(p) {
|
|
2482
|
+
if (typeof p === "string") return Buffer.from(p, "utf8");
|
|
2483
|
+
if (Buffer.isBuffer(p)) return p;
|
|
2484
|
+
return Buffer.from(p);
|
|
2485
|
+
}
|
|
2486
|
+
function parseHeader(header) {
|
|
2487
|
+
let t = NaN;
|
|
2488
|
+
const v1 = [];
|
|
2489
|
+
for (const part of header.split(",")) {
|
|
2490
|
+
const [k, v] = part.split("=", 2);
|
|
2491
|
+
if (!k || v === void 0) continue;
|
|
2492
|
+
const key = k.trim();
|
|
2493
|
+
const value = v.trim();
|
|
2494
|
+
if (key === "t") t = Number(value);
|
|
2495
|
+
else if (key === "v1") v1.push(value);
|
|
2496
|
+
}
|
|
2497
|
+
return { t, v1 };
|
|
2498
|
+
}
|
|
2499
|
+
function timingSafeEqualHex(a, b) {
|
|
2500
|
+
if (a.length !== b.length) return false;
|
|
2501
|
+
try {
|
|
2502
|
+
return import_crypto3.default.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
2503
|
+
} catch {
|
|
2504
|
+
return false;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function verifyWebhookSignature(opts) {
|
|
2508
|
+
const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
|
|
2509
|
+
if (!headerRaw || typeof headerRaw !== "string") {
|
|
2510
|
+
throw new WebhookSignatureError("MISSING_HEADER", "Missing X-IQAuth-Signature header");
|
|
2511
|
+
}
|
|
2512
|
+
if (!opts.secret) {
|
|
2513
|
+
throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
|
|
2514
|
+
}
|
|
2515
|
+
const { t, v1 } = parseHeader(headerRaw);
|
|
2516
|
+
if (!Number.isFinite(t) || v1.length === 0) {
|
|
2517
|
+
throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
|
|
2518
|
+
}
|
|
2519
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
2520
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
2521
|
+
if (Math.abs(now - t) > tolerance) {
|
|
2522
|
+
throw new WebhookSignatureError(
|
|
2523
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
2524
|
+
`Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
const body = toBuffer(opts.payload);
|
|
2528
|
+
const expected = import_crypto3.default.createHmac("sha256", opts.secret).update(`${t}.`).update(body).digest("hex");
|
|
2529
|
+
const matched = v1.some((sig) => timingSafeEqualHex(sig, expected));
|
|
2530
|
+
if (!matched) {
|
|
2531
|
+
throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
|
|
2532
|
+
}
|
|
2533
|
+
let parsed;
|
|
2534
|
+
try {
|
|
2535
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
2536
|
+
} catch {
|
|
2537
|
+
throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
|
|
2538
|
+
}
|
|
2539
|
+
return parsed;
|
|
2540
|
+
}
|
|
2541
|
+
function isValidWebhookSignature(opts) {
|
|
2542
|
+
try {
|
|
2543
|
+
verifyWebhookSignature(opts);
|
|
2544
|
+
return true;
|
|
2545
|
+
} catch {
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// src/server/provisioningBridge.ts
|
|
2551
|
+
function defaultIsUniqueViolation(err) {
|
|
2552
|
+
if (!err || typeof err !== "object") return false;
|
|
2553
|
+
const e = err;
|
|
2554
|
+
if (e.code === "23505") return true;
|
|
2555
|
+
if (typeof e.code === "string" && e.code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
2556
|
+
if (typeof e.message === "string" && /unique constraint|duplicate key/i.test(e.message)) return true;
|
|
2557
|
+
return false;
|
|
2558
|
+
}
|
|
2559
|
+
function createProvisioningBridge(options) {
|
|
2560
|
+
const { storage } = options;
|
|
2561
|
+
const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
|
|
2562
|
+
const roleOf = (claims) => {
|
|
2563
|
+
try {
|
|
2564
|
+
return options.roleMapper?.(claims) ?? null;
|
|
2565
|
+
} catch {
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
const ensureUser = async (claims) => {
|
|
2570
|
+
if (!claims?.sub) {
|
|
2571
|
+
throw new Error("createProvisioningBridge: claims.sub is required");
|
|
2572
|
+
}
|
|
2573
|
+
const byId = await storage.findByIqAuthUserId(claims.sub);
|
|
2574
|
+
if (byId) return { user: byId, claims, created: false, adopted: false };
|
|
2575
|
+
if (claims.email) {
|
|
2576
|
+
const byEmail = await storage.findByEmail(claims.email);
|
|
2577
|
+
if (byEmail) {
|
|
2578
|
+
if (storage.adoptByEmail) {
|
|
2579
|
+
const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
|
|
2580
|
+
return { user: adopted, claims, created: false, adopted: true };
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
try {
|
|
2585
|
+
const created = await storage.insertFromClaims(claims, roleOf(claims));
|
|
2586
|
+
return { user: created, claims, created: true, adopted: false };
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
if (!isUniqueViolation(err)) throw err;
|
|
2589
|
+
const after = await storage.findByIqAuthUserId(claims.sub);
|
|
2590
|
+
if (after) return { user: after, claims, created: false, adopted: false };
|
|
2591
|
+
if (claims.email) {
|
|
2592
|
+
const byEmail = await storage.findByEmail(claims.email);
|
|
2593
|
+
if (byEmail) return { user: byEmail, claims, created: false, adopted: true };
|
|
2594
|
+
}
|
|
2595
|
+
throw err;
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
return { ensureUser };
|
|
2599
|
+
}
|
|
2089
2600
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2090
2601
|
0 && (module.exports = {
|
|
2091
2602
|
ApiKeysModule,
|
|
@@ -2118,11 +2629,17 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2118
2629
|
TokensModule,
|
|
2119
2630
|
UsersModule,
|
|
2120
2631
|
VendorsModule,
|
|
2632
|
+
WebhookSignatureError,
|
|
2121
2633
|
WebhooksModule,
|
|
2122
2634
|
assertPublishableKey,
|
|
2635
|
+
createProvisioningBridge,
|
|
2636
|
+
createTestIssuer,
|
|
2123
2637
|
encodePublishableKey,
|
|
2124
2638
|
iqAuthMiddleware,
|
|
2125
2639
|
isPublishableKey,
|
|
2126
2640
|
isSecretKey,
|
|
2127
|
-
|
|
2641
|
+
isValidWebhookSignature,
|
|
2642
|
+
parsePublishableKey,
|
|
2643
|
+
verifyWebhookSignature,
|
|
2644
|
+
verifyWsUpgrade
|
|
2128
2645
|
});
|