@iqauth/sdk 2.6.4 → 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 +5 -5
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- 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-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-XAWYUPMO.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/{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 +313 -33
- package/dist/react.mjs +58 -2632
- 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-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/{signIn-CiIBTJIh.d.mts → 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,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
|
+
};
|
|
@@ -91,7 +91,7 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
91
91
|
returnTo,
|
|
92
92
|
createdAt: Date.now()
|
|
93
93
|
});
|
|
94
|
-
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.
|
|
94
|
+
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
|
|
95
95
|
url.searchParams.set("response_type", "code");
|
|
96
96
|
url.searchParams.set("app", manager.appKey);
|
|
97
97
|
url.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
@@ -106,33 +106,50 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
106
106
|
return url.toString();
|
|
107
107
|
}
|
|
108
108
|
async function redirectToSignIn(manager, opts = {}) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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;
|
|
112
124
|
}
|
|
113
|
-
window.location.assign(url);
|
|
114
125
|
}
|
|
115
126
|
async function signIn(manager, opts = {}) {
|
|
116
127
|
return redirectToSignIn(manager, opts);
|
|
117
128
|
}
|
|
118
129
|
async function handleAuthCallback(manager, options = {}) {
|
|
130
|
+
const t0 = Date.now();
|
|
131
|
+
const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
|
|
119
132
|
const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
120
133
|
const code = url.searchParams.get("code");
|
|
121
134
|
const state = url.searchParams.get("state");
|
|
122
135
|
const errorParam = url.searchParams.get("error");
|
|
123
136
|
if (errorParam) {
|
|
137
|
+
emit(false, errorParam);
|
|
124
138
|
return { ok: false, returnTo: "/", error: errorParam };
|
|
125
139
|
}
|
|
126
140
|
if (!code || !state) {
|
|
141
|
+
emit(false, "missing_code_or_state");
|
|
127
142
|
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
128
143
|
}
|
|
129
144
|
const record = loadPkce(state);
|
|
130
145
|
if (!record) {
|
|
146
|
+
emit(false, "unknown_state");
|
|
131
147
|
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
132
148
|
}
|
|
133
149
|
clearPkce(state);
|
|
134
150
|
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
135
151
|
if (!fetchImpl) {
|
|
152
|
+
emit(false, "no_fetch");
|
|
136
153
|
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
137
154
|
}
|
|
138
155
|
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
@@ -151,10 +168,12 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
151
168
|
const body = await res.json().catch(() => ({}));
|
|
152
169
|
if (!res.ok) {
|
|
153
170
|
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
171
|
+
emit(false, desc);
|
|
154
172
|
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
155
173
|
}
|
|
156
174
|
const tokens = body;
|
|
157
175
|
if (!tokens.access_token) {
|
|
176
|
+
emit(false, "missing_access_token");
|
|
158
177
|
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
159
178
|
}
|
|
160
179
|
if (tokens.refresh_token) {
|
|
@@ -162,21 +181,24 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
162
181
|
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
163
182
|
}
|
|
164
183
|
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
184
|
+
emit(true);
|
|
165
185
|
return { ok: true, returnTo: record.returnTo };
|
|
166
186
|
}
|
|
167
187
|
async function signOut(manager, opts = {}) {
|
|
168
188
|
if (!opts.localOnly) {
|
|
169
189
|
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
190
|
+
const idempotency = manager.getIdempotencyToken();
|
|
170
191
|
try {
|
|
171
192
|
const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
172
|
-
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
193
|
+
await manager.fetch(url, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
|
|
173
194
|
} catch {
|
|
174
195
|
}
|
|
175
196
|
if (opts.endSsoSession !== false) {
|
|
176
197
|
try {
|
|
177
198
|
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
178
199
|
method: "POST",
|
|
179
|
-
credentials: "include"
|
|
200
|
+
credentials: "include",
|
|
201
|
+
headers: { "X-IQAuth-Idempotency": idempotency }
|
|
180
202
|
}).catch(() => void 0);
|
|
181
203
|
} catch {
|
|
182
204
|
}
|
|
@@ -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);
|
|
@@ -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";
|
|
@@ -23,6 +23,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
23
23
|
"iqvalidate"
|
|
24
24
|
];
|
|
25
25
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
26
|
+
function classifyJoseError(err) {
|
|
27
|
+
if (err instanceof joseErrors.JWTExpired) {
|
|
28
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
29
|
+
}
|
|
30
|
+
if (err instanceof joseErrors.JOSEError) {
|
|
31
|
+
return { code: "token_invalid", message: err.message };
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof Error) {
|
|
34
|
+
return { code: "token_invalid", message: err.message };
|
|
35
|
+
}
|
|
36
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
37
|
+
}
|
|
26
38
|
function decodeProtectedHeader(token) {
|
|
27
39
|
const parts = token.split(".");
|
|
28
40
|
if (parts.length < 2) return null;
|
|
@@ -59,11 +71,11 @@ var TokensModule = class {
|
|
|
59
71
|
async verify(token, options = {}) {
|
|
60
72
|
const header = decodeProtectedHeader(token);
|
|
61
73
|
if (!header) {
|
|
62
|
-
throw new IQAuthError("
|
|
74
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
63
75
|
}
|
|
64
76
|
const kid = header.kid;
|
|
65
77
|
if (!kid) {
|
|
66
|
-
throw new IQAuthError("
|
|
78
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
67
79
|
}
|
|
68
80
|
let cache = await this.ensureCache();
|
|
69
81
|
if (!cache.byKid.has(kid)) {
|
|
@@ -71,7 +83,7 @@ var TokensModule = class {
|
|
|
71
83
|
cache = await this.ensureCache();
|
|
72
84
|
}
|
|
73
85
|
if (!cache.byKid.has(kid)) {
|
|
74
|
-
throw new IQAuthError("
|
|
86
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
75
87
|
}
|
|
76
88
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
77
89
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -87,16 +99,8 @@ var TokensModule = class {
|
|
|
87
99
|
const { payload } = await jwtVerify(token, cache.verifier, verifyOptions);
|
|
88
100
|
return payload;
|
|
89
101
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (err instanceof joseErrors.JOSEError) {
|
|
94
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
95
|
-
}
|
|
96
|
-
if (err instanceof Error) {
|
|
97
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
98
|
-
}
|
|
99
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
102
|
+
const classified = classifyJoseError(err);
|
|
103
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
/**
|
|
@@ -138,7 +142,7 @@ var TokensModule = class {
|
|
|
138
142
|
getClaims(token) {
|
|
139
143
|
const claims = this.decode(token);
|
|
140
144
|
if (!claims) {
|
|
141
|
-
throw new IQAuthError("
|
|
145
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
142
146
|
}
|
|
143
147
|
return claims;
|
|
144
148
|
}
|
|
@@ -148,7 +152,7 @@ var TokensModule = class {
|
|
|
148
152
|
}
|
|
149
153
|
await this.refreshJwks();
|
|
150
154
|
if (!this.jwksCache) {
|
|
151
|
-
throw new IQAuthError("
|
|
155
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
152
156
|
}
|
|
153
157
|
return this.jwksCache;
|
|
154
158
|
}
|
|
@@ -158,22 +162,38 @@ var TokensModule = class {
|
|
|
158
162
|
}
|
|
159
163
|
this.inFlightRefresh = (async () => {
|
|
160
164
|
try {
|
|
161
|
-
|
|
165
|
+
let res;
|
|
166
|
+
try {
|
|
167
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
throw new IQAuthError(
|
|
170
|
+
"network",
|
|
171
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
172
|
+
void 0,
|
|
173
|
+
err
|
|
174
|
+
);
|
|
175
|
+
}
|
|
162
176
|
if (!res.ok) {
|
|
163
177
|
throw new IQAuthError(
|
|
164
|
-
"
|
|
165
|
-
`Failed to fetch JWKS: ${res.status}
|
|
178
|
+
"jwks_fetch_failed",
|
|
179
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
180
|
+
res.status
|
|
166
181
|
);
|
|
167
182
|
}
|
|
168
183
|
let jwks;
|
|
169
184
|
try {
|
|
170
185
|
jwks = await res.json();
|
|
171
|
-
} catch {
|
|
172
|
-
throw new IQAuthError(
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new IQAuthError(
|
|
188
|
+
"jwks_fetch_failed",
|
|
189
|
+
"Malformed JWKS response: invalid JSON",
|
|
190
|
+
res.status,
|
|
191
|
+
err
|
|
192
|
+
);
|
|
173
193
|
}
|
|
174
194
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
175
195
|
throw new IQAuthError(
|
|
176
|
-
"
|
|
196
|
+
"jwks_fetch_failed",
|
|
177
197
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
178
198
|
);
|
|
179
199
|
}
|
|
@@ -181,7 +201,7 @@ var TokensModule = class {
|
|
|
181
201
|
for (const key of jwks.keys) {
|
|
182
202
|
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")) {
|
|
183
203
|
throw new IQAuthError(
|
|
184
|
-
"
|
|
204
|
+
"jwks_fetch_failed",
|
|
185
205
|
"Malformed JWKS response: key missing required fields"
|
|
186
206
|
);
|
|
187
207
|
}
|
|
@@ -199,6 +219,19 @@ var TokensModule = class {
|
|
|
199
219
|
clearCache() {
|
|
200
220
|
this.jwksCache = null;
|
|
201
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
224
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
225
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
226
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
227
|
+
*/
|
|
228
|
+
async prewarm() {
|
|
229
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
230
|
+
try {
|
|
231
|
+
await this.refreshJwks();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
202
235
|
};
|
|
203
236
|
|
|
204
237
|
export {
|