@iqauth/sdk 2.6.3 → 2.7.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 +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +181 -41
- 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 +271 -32
- package/dist/browser.mjs +10 -8
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/chunk-C2ZTBOAC.mjs +36 -0
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/chunk-PMAFENVI.mjs +229 -0
- package/dist/chunk-RR2MGPTK.mjs +2724 -0
- package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
- package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
- package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
- 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-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
- package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +349 -52
- package/dist/express.mjs +39 -12
- package/dist/fastify.d.mts +2 -0
- package/dist/fastify.d.ts +2 -0
- package/dist/fastify.js +332 -52
- package/dist/fastify.mjs +23 -8
- package/dist/hono.d.mts +2 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.js +329 -52
- package/dist/hono.mjs +20 -8
- package/dist/index-5KSZEnDe.d.ts +1626 -0
- package/dist/index-CKoZHAoc.d.mts +1626 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +565 -69
- package/dist/index.mjs +29 -9
- 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/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +276 -41
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +391 -201
- package/dist/next.mjs +22 -7
- package/dist/pkce-7WKV4OIN.mjs +11 -0
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
- 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 +97 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +343 -36
- package/dist/react.mjs +59 -2611
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +148 -3
- package/dist/server/handlers.d.ts +148 -3
- package/dist/server/handlers.js +410 -11
- package/dist/server/handlers.mjs +12 -3
- package/dist/server.d.mts +151 -8
- package/dist/server.d.ts +151 -8
- package/dist/server.js +406 -50
- package/dist/server.mjs +93 -11
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +181 -41
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
- package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
- package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
- package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
- package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
- package/dist/webhooks.d.mts +100 -17
- package/dist/webhooks.d.ts +100 -17
- package/dist/webhooks.js +164 -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/package.json +13 -3
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/browser/pkce.ts
|
|
2
|
+
function getCrypto() {
|
|
3
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
4
|
+
return globalThis.crypto;
|
|
5
|
+
}
|
|
6
|
+
throw new Error("WebCrypto is not available in this environment");
|
|
7
|
+
}
|
|
8
|
+
function base64UrlEncode(bytes) {
|
|
9
|
+
let bin = "";
|
|
10
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
11
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
12
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
function randomUrlSafe(byteLength = 32) {
|
|
15
|
+
const bytes = new Uint8Array(byteLength);
|
|
16
|
+
getCrypto().getRandomValues(bytes);
|
|
17
|
+
return base64UrlEncode(bytes);
|
|
18
|
+
}
|
|
19
|
+
async function s256Challenge(verifier) {
|
|
20
|
+
const data = new TextEncoder().encode(verifier);
|
|
21
|
+
const digest = await getCrypto().subtle.digest("SHA-256", data);
|
|
22
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
23
|
+
}
|
|
24
|
+
async function createPkcePair() {
|
|
25
|
+
const codeVerifier = randomUrlSafe(32);
|
|
26
|
+
const codeChallenge = await s256Challenge(codeVerifier);
|
|
27
|
+
const state = randomUrlSafe(16);
|
|
28
|
+
const nonce = randomUrlSafe(16);
|
|
29
|
+
return { codeVerifier, codeChallenge, state, nonce };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
randomUrlSafe,
|
|
34
|
+
s256Challenge,
|
|
35
|
+
createPkcePair
|
|
36
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/permissions/wildcard.ts
|
|
2
|
+
var SUFFIX = ".*";
|
|
3
|
+
function wildcardPrefix(pattern) {
|
|
4
|
+
return pattern.slice(0, -SUFFIX.length);
|
|
5
|
+
}
|
|
6
|
+
function hasPermission(set, id) {
|
|
7
|
+
if (!id) return false;
|
|
8
|
+
if (!set) return false;
|
|
9
|
+
if (id === "*") {
|
|
10
|
+
for (const entry of set) if (entry === "*") return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const queryIsWildcard = id.endsWith(SUFFIX);
|
|
14
|
+
const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
|
|
15
|
+
for (const entry of set) {
|
|
16
|
+
if (!entry) continue;
|
|
17
|
+
if (entry === "*") return true;
|
|
18
|
+
if (entry === id) return true;
|
|
19
|
+
if (entry.endsWith(SUFFIX)) {
|
|
20
|
+
const prefix = wildcardPrefix(entry);
|
|
21
|
+
if (!queryIsWildcard) {
|
|
22
|
+
if (id === prefix) return true;
|
|
23
|
+
if (id.startsWith(prefix + ".")) return true;
|
|
24
|
+
} else {
|
|
25
|
+
if (queryPrefix === prefix) return true;
|
|
26
|
+
if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function expandPermissions(set) {
|
|
33
|
+
if (!set) return [];
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const raw of set) {
|
|
36
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
37
|
+
seen.add(raw);
|
|
38
|
+
}
|
|
39
|
+
if (seen.has("*")) return ["*"];
|
|
40
|
+
const wildcards = [];
|
|
41
|
+
for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const entry of seen) {
|
|
44
|
+
let covered = false;
|
|
45
|
+
for (const w of wildcards) {
|
|
46
|
+
if (w === entry) continue;
|
|
47
|
+
const prefix = wildcardPrefix(w);
|
|
48
|
+
if (entry === prefix) {
|
|
49
|
+
covered = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (entry.startsWith(prefix + ".")) {
|
|
53
|
+
covered = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!covered) out.push(entry);
|
|
58
|
+
}
|
|
59
|
+
out.sort();
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
hasPermission,
|
|
65
|
+
expandPermissions
|
|
66
|
+
};
|
|
@@ -1,33 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return globalThis.crypto;
|
|
5
|
-
}
|
|
6
|
-
throw new Error("WebCrypto is not available in this environment");
|
|
7
|
-
}
|
|
8
|
-
function base64UrlEncode(bytes) {
|
|
9
|
-
let bin = "";
|
|
10
|
-
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
11
|
-
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
12
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
13
|
-
}
|
|
14
|
-
function randomUrlSafe(byteLength = 32) {
|
|
15
|
-
const bytes = new Uint8Array(byteLength);
|
|
16
|
-
getCrypto().getRandomValues(bytes);
|
|
17
|
-
return base64UrlEncode(bytes);
|
|
18
|
-
}
|
|
19
|
-
async function s256Challenge(verifier) {
|
|
20
|
-
const data = new TextEncoder().encode(verifier);
|
|
21
|
-
const digest = await getCrypto().subtle.digest("SHA-256", data);
|
|
22
|
-
return base64UrlEncode(new Uint8Array(digest));
|
|
23
|
-
}
|
|
24
|
-
async function createPkcePair() {
|
|
25
|
-
const codeVerifier = randomUrlSafe(32);
|
|
26
|
-
const codeChallenge = await s256Challenge(codeVerifier);
|
|
27
|
-
const state = randomUrlSafe(16);
|
|
28
|
-
const nonce = randomUrlSafe(16);
|
|
29
|
-
return { codeVerifier, codeChallenge, state, nonce };
|
|
30
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createPkcePair
|
|
3
|
+
} from "./chunk-C2ZTBOAC.mjs";
|
|
31
4
|
|
|
32
5
|
// src/browser/storage.ts
|
|
33
6
|
var REFRESH_COOKIE = "iqauth_rt";
|
|
@@ -118,7 +91,7 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
118
91
|
returnTo,
|
|
119
92
|
createdAt: Date.now()
|
|
120
93
|
});
|
|
121
|
-
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.
|
|
94
|
+
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
|
|
122
95
|
url.searchParams.set("response_type", "code");
|
|
123
96
|
url.searchParams.set("app", manager.appKey);
|
|
124
97
|
url.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
@@ -133,33 +106,50 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
133
106
|
return url.toString();
|
|
134
107
|
}
|
|
135
108
|
async function redirectToSignIn(manager, opts = {}) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
109
|
+
const t0 = Date.now();
|
|
110
|
+
let ok = false;
|
|
111
|
+
let code;
|
|
112
|
+
try {
|
|
113
|
+
const url = await buildSignInUrl(manager, opts);
|
|
114
|
+
if (typeof window === "undefined") {
|
|
115
|
+
code = "NO_WINDOW";
|
|
116
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
117
|
+
}
|
|
118
|
+
ok = true;
|
|
119
|
+
manager.recordTiming("signIn", Date.now() - t0, true);
|
|
120
|
+
window.location.assign(url);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (!ok) manager.recordTiming("signIn", Date.now() - t0, false, code ?? (err instanceof Error ? err.message : "ERROR"));
|
|
123
|
+
throw err;
|
|
139
124
|
}
|
|
140
|
-
window.location.assign(url);
|
|
141
125
|
}
|
|
142
126
|
async function signIn(manager, opts = {}) {
|
|
143
127
|
return redirectToSignIn(manager, opts);
|
|
144
128
|
}
|
|
145
129
|
async function handleAuthCallback(manager, options = {}) {
|
|
130
|
+
const t0 = Date.now();
|
|
131
|
+
const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
|
|
146
132
|
const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
147
133
|
const code = url.searchParams.get("code");
|
|
148
134
|
const state = url.searchParams.get("state");
|
|
149
135
|
const errorParam = url.searchParams.get("error");
|
|
150
136
|
if (errorParam) {
|
|
137
|
+
emit(false, errorParam);
|
|
151
138
|
return { ok: false, returnTo: "/", error: errorParam };
|
|
152
139
|
}
|
|
153
140
|
if (!code || !state) {
|
|
141
|
+
emit(false, "missing_code_or_state");
|
|
154
142
|
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
155
143
|
}
|
|
156
144
|
const record = loadPkce(state);
|
|
157
145
|
if (!record) {
|
|
146
|
+
emit(false, "unknown_state");
|
|
158
147
|
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
159
148
|
}
|
|
160
149
|
clearPkce(state);
|
|
161
150
|
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
162
151
|
if (!fetchImpl) {
|
|
152
|
+
emit(false, "no_fetch");
|
|
163
153
|
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
164
154
|
}
|
|
165
155
|
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
@@ -178,10 +168,12 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
178
168
|
const body = await res.json().catch(() => ({}));
|
|
179
169
|
if (!res.ok) {
|
|
180
170
|
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
171
|
+
emit(false, desc);
|
|
181
172
|
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
182
173
|
}
|
|
183
174
|
const tokens = body;
|
|
184
175
|
if (!tokens.access_token) {
|
|
176
|
+
emit(false, "missing_access_token");
|
|
185
177
|
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
186
178
|
}
|
|
187
179
|
if (tokens.refresh_token) {
|
|
@@ -189,21 +181,24 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
189
181
|
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
190
182
|
}
|
|
191
183
|
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
184
|
+
emit(true);
|
|
192
185
|
return { ok: true, returnTo: record.returnTo };
|
|
193
186
|
}
|
|
194
187
|
async function signOut(manager, opts = {}) {
|
|
195
188
|
if (!opts.localOnly) {
|
|
196
189
|
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
190
|
+
const idempotency = manager.getIdempotencyToken();
|
|
197
191
|
try {
|
|
198
192
|
const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
199
|
-
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
193
|
+
await manager.fetch(url, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
|
|
200
194
|
} catch {
|
|
201
195
|
}
|
|
202
196
|
if (opts.endSsoSession !== false) {
|
|
203
197
|
try {
|
|
204
198
|
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
205
199
|
method: "POST",
|
|
206
|
-
credentials: "include"
|
|
200
|
+
credentials: "include",
|
|
201
|
+
headers: { "X-IQAuth-Idempotency": idempotency }
|
|
207
202
|
}).catch(() => void 0);
|
|
208
203
|
} catch {
|
|
209
204
|
}
|
|
@@ -221,9 +216,6 @@ export {
|
|
|
221
216
|
setCookie,
|
|
222
217
|
getCookie,
|
|
223
218
|
clearCookie,
|
|
224
|
-
randomUrlSafe,
|
|
225
|
-
s256Challenge,
|
|
226
|
-
createPkcePair,
|
|
227
219
|
buildSignInUrl,
|
|
228
220
|
redirectToSignIn,
|
|
229
221
|
signIn,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IQAuthError
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
4
4
|
import {
|
|
5
5
|
__require
|
|
6
6
|
} from "./chunk-Y6FXYEAI.mjs";
|
|
@@ -64,14 +64,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
64
64
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
65
65
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
66
66
|
throw new IQAuthError(
|
|
67
|
-
"
|
|
67
|
+
"config_invalid",
|
|
68
68
|
`${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.`
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
72
72
|
if (!shapeMatch) {
|
|
73
73
|
throw new IQAuthError(
|
|
74
|
-
"
|
|
74
|
+
"config_invalid",
|
|
75
75
|
`${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.`
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -80,19 +80,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
80
80
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
81
81
|
} catch {
|
|
82
82
|
throw new IQAuthError(
|
|
83
|
-
"
|
|
83
|
+
"config_invalid",
|
|
84
84
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
if (!isPublishableKeyPayload(decoded)) {
|
|
88
88
|
throw new IQAuthError(
|
|
89
|
-
"
|
|
89
|
+
"config_invalid",
|
|
90
90
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
94
94
|
throw new IQAuthError(
|
|
95
|
-
"
|
|
95
|
+
"config_invalid",
|
|
96
96
|
`${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.`
|
|
97
97
|
);
|
|
98
98
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TokensModule
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-NUO2I65G.mjs";
|
|
4
4
|
import {
|
|
5
5
|
IQAuthError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
7
7
|
|
|
8
8
|
// src/modules/auth.ts
|
|
9
9
|
function parseLoginResponse(data, browserSessionMode) {
|
|
@@ -484,14 +484,14 @@ var OidcModule = class {
|
|
|
484
484
|
*/
|
|
485
485
|
async handleCallback(params) {
|
|
486
486
|
if (!params.state) {
|
|
487
|
-
throw new IQAuthError("
|
|
487
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
488
488
|
}
|
|
489
489
|
if (!params.code) {
|
|
490
|
-
throw new IQAuthError("
|
|
490
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
491
491
|
}
|
|
492
492
|
const stored = await this.stateStore.get(params.state);
|
|
493
493
|
if (!stored) {
|
|
494
|
-
throw new IQAuthError("
|
|
494
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
495
495
|
}
|
|
496
496
|
let tokens;
|
|
497
497
|
try {
|
|
@@ -509,7 +509,7 @@ var OidcModule = class {
|
|
|
509
509
|
if (tokens.id_token) {
|
|
510
510
|
if (!this.tokensModule) {
|
|
511
511
|
throw new IQAuthError(
|
|
512
|
-
"
|
|
512
|
+
"config_invalid",
|
|
513
513
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
514
514
|
);
|
|
515
515
|
}
|
|
@@ -520,7 +520,7 @@ var OidcModule = class {
|
|
|
520
520
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
521
521
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
522
522
|
throw new IQAuthError(
|
|
523
|
-
"
|
|
523
|
+
"token_invalid",
|
|
524
524
|
"OIDC id_token nonce did not match the stored value"
|
|
525
525
|
);
|
|
526
526
|
}
|
|
@@ -721,6 +721,9 @@ var AppsModule = class {
|
|
|
721
721
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
722
722
|
*/
|
|
723
723
|
async get(appKey) {
|
|
724
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
725
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
726
|
+
}
|
|
724
727
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
725
728
|
}
|
|
726
729
|
/**
|
|
@@ -740,6 +743,16 @@ var AppsModule = class {
|
|
|
740
743
|
401
|
|
741
744
|
);
|
|
742
745
|
}
|
|
746
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
747
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
748
|
+
}
|
|
749
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
750
|
+
throw new IQAuthError(
|
|
751
|
+
"ENVIRONMENT_REQUIRED",
|
|
752
|
+
"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.",
|
|
753
|
+
400
|
|
754
|
+
);
|
|
755
|
+
}
|
|
743
756
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
744
757
|
}
|
|
745
758
|
/**
|
|
@@ -749,11 +762,14 @@ var AppsModule = class {
|
|
|
749
762
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
750
763
|
*/
|
|
751
764
|
async isRegistered(appKey) {
|
|
765
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
766
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
767
|
+
}
|
|
752
768
|
try {
|
|
753
769
|
await this.get(appKey);
|
|
754
770
|
return true;
|
|
755
771
|
} catch (err) {
|
|
756
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
772
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
757
773
|
return false;
|
|
758
774
|
}
|
|
759
775
|
throw err;
|
|
@@ -790,6 +806,20 @@ var RolesModule = class {
|
|
|
790
806
|
};
|
|
791
807
|
|
|
792
808
|
// src/modules/permissionGroups.ts
|
|
809
|
+
function assertAppKey(appKey, callsite) {
|
|
810
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
811
|
+
throw new IQAuthError(
|
|
812
|
+
"VALIDATION_ERROR",
|
|
813
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
814
|
+
400
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
819
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
820
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
793
823
|
var PermissionGroupsModule = class {
|
|
794
824
|
constructor(http) {
|
|
795
825
|
this.http = http;
|
|
@@ -810,7 +840,14 @@ var PermissionGroupsModule = class {
|
|
|
810
840
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
811
841
|
}
|
|
812
842
|
async addPermission(tenantId, groupId, data) {
|
|
813
|
-
|
|
843
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
844
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
845
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
846
|
+
appKey: data.appKey,
|
|
847
|
+
nodeKey: data.nodeKey,
|
|
848
|
+
effect: data.effect,
|
|
849
|
+
weight: data.weight
|
|
850
|
+
});
|
|
814
851
|
}
|
|
815
852
|
async removePermission(tenantId, groupId, permissionId) {
|
|
816
853
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -834,21 +871,51 @@ var PermissionGroupsModule = class {
|
|
|
834
871
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
835
872
|
}
|
|
836
873
|
async addUserOverride(tenantId, userId, data) {
|
|
837
|
-
|
|
874
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
875
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
876
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
877
|
+
appKey: data.appKey,
|
|
878
|
+
nodeKey: data.nodeKey,
|
|
879
|
+
effect: data.effect,
|
|
880
|
+
weight: data.weight,
|
|
881
|
+
expiresAt: data.expiresAt
|
|
882
|
+
});
|
|
838
883
|
}
|
|
839
884
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
840
885
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
841
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
889
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
890
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
891
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
892
|
+
*/
|
|
842
893
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const qs = query.toString();
|
|
847
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
894
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
895
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
896
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
848
897
|
}
|
|
849
898
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
899
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
900
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
850
901
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
851
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
905
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
906
|
+
* single misconfigured entry can't slip through and silently report
|
|
907
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
908
|
+
*/
|
|
909
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
910
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
911
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
912
|
+
}
|
|
913
|
+
checks.forEach((c, i) => {
|
|
914
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
915
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
916
|
+
});
|
|
917
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
918
|
+
}
|
|
852
919
|
};
|
|
853
920
|
|
|
854
921
|
// src/modules/apiKeys.ts
|
|
@@ -1365,7 +1432,7 @@ var HttpClient = class {
|
|
|
1365
1432
|
headers: this.buildHeaders(),
|
|
1366
1433
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
1367
1434
|
const refreshToken = this.config.getRefreshToken();
|
|
1368
|
-
if (!refreshToken) throw new IQAuthError("
|
|
1435
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
1369
1436
|
return { body: JSON.stringify({ refreshToken }) };
|
|
1370
1437
|
})()
|
|
1371
1438
|
});
|
|
@@ -1382,7 +1449,7 @@ var HttpClient = class {
|
|
|
1382
1449
|
return;
|
|
1383
1450
|
}
|
|
1384
1451
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
1385
|
-
throw new IQAuthError("
|
|
1452
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
1386
1453
|
}
|
|
1387
1454
|
const tokens = {
|
|
1388
1455
|
accessToken: body.data.accessToken,
|
|
@@ -1400,7 +1467,7 @@ var HttpClient = class {
|
|
|
1400
1467
|
return this.requestWithRetry(method, path, body, options, false);
|
|
1401
1468
|
}
|
|
1402
1469
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
1403
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
1470
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
1404
1471
|
await this.attemptRefresh();
|
|
1405
1472
|
}
|
|
1406
1473
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -1475,6 +1542,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1475
1542
|
this._refreshToken = tokens.refreshToken;
|
|
1476
1543
|
},
|
|
1477
1544
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1545
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1546
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1547
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1548
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1478
1549
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1479
1550
|
sessionHeaderName: config.sessionHeaderName,
|
|
1480
1551
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1515,6 +1586,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1515
1586
|
static forServer(config) {
|
|
1516
1587
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1517
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1591
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1592
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1593
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1594
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1595
|
+
*/
|
|
1518
1596
|
static forMobile(config) {
|
|
1519
1597
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1520
1598
|
}
|
|
@@ -1531,6 +1609,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1531
1609
|
getRefreshToken() {
|
|
1532
1610
|
return this._refreshToken;
|
|
1533
1611
|
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1614
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1615
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1616
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1617
|
+
*/
|
|
1618
|
+
async prewarm() {
|
|
1619
|
+
await Promise.all([
|
|
1620
|
+
this.tokens.prewarm(),
|
|
1621
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1622
|
+
]);
|
|
1623
|
+
}
|
|
1534
1624
|
getCurrentClaims() {
|
|
1535
1625
|
if (!this._accessToken) return null;
|
|
1536
1626
|
return this.tokens.decode(this._accessToken);
|