@iqauth/sdk 2.3.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 +110 -0
- package/dist/browser-session.d.mts +3 -2
- package/dist/browser-session.d.ts +3 -2
- package/dist/browser.d.mts +64 -29
- package/dist/browser.d.ts +64 -29
- package/dist/browser.js +782 -38
- package/dist/browser.mjs +43 -3
- package/dist/bundle-LUKDQYVQ.mjs +374 -0
- package/dist/chunk-3JULWS6F.mjs +106 -0
- package/dist/chunk-5T7GHBX6.mjs +1165 -0
- package/dist/{chunk-KGEPDXHU.mjs → chunk-6TDJJER7.mjs} +2 -2
- package/dist/{chunk-RACIPVLD.mjs → chunk-76W5TLQQ.mjs} +262 -220
- package/dist/{chunk-EKTNEZIH.mjs → chunk-BVV54LPI.mjs} +37 -5
- 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/cli/index.js +144 -36
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DTX4hNdS.d.ts → client-BNQe3AgF.d.ts} +3 -62
- package/dist/{client-vdh2a9fJ.d.mts → client-kYlJFgPv.d.mts} +3 -62
- package/dist/doctor-YYNHNMLD.mjs +198 -0
- package/dist/{express-A0-dWEMy.d.mts → express-B6_1vBYZ.d.mts} +23 -2
- package/dist/{express-Bo_pJKHN.d.ts → express-CHpfa7D_.d.ts} +23 -2
- package/dist/express.d.mts +5 -4
- package/dist/express.d.ts +5 -4
- package/dist/express.js +36 -4
- package/dist/express.mjs +8 -8
- package/dist/fastify.js +2 -2
- package/dist/fastify.mjs +4 -4
- package/dist/hono.js +2 -2
- package/dist/hono.mjs +4 -4
- package/dist/index.d.mts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +500 -4
- package/dist/index.mjs +29 -9
- 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 -2
- package/dist/mobile.d.ts +3 -2
- package/dist/next.d.mts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +2 -2
- package/dist/next.mjs +1 -1
- 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 +2985 -567
- package/dist/react.mjs +1517 -94
- package/dist/reverify-4UEJXUS6.mjs +16 -0
- package/dist/server/handlers.d.mts +10 -1
- package/dist/server/handlers.d.ts +10 -1
- package/dist/server/handlers.js +2 -2
- package/dist/server/handlers.mjs +1 -1
- package/dist/server.d.mts +5 -3
- package/dist/server.d.ts +5 -3
- package/dist/server.js +89 -4
- package/dist/server.mjs +12 -8
- package/dist/service.d.mts +3 -2
- package/dist/service.d.ts +3 -2
- package/dist/signIn-CCY4JE5G.mjs +15 -0
- package/dist/{signIn-Cd0P4y9d.d.mts → signIn-CiIBTJIh.d.mts} +224 -4
- package/dist/{signIn-DKakyzeu.d.ts → signIn-OCr88Zf8.d.ts} +224 -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.mts → types-DZAflmmq.d.mts} +6 -0
- package/dist/{types-Cxl3bQHt.d.ts → 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 +22 -2
- package/dist/doctor-A5E7LSFW.mjs +0 -90
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IQAuthError
|
|
3
|
+
} from "./chunk-6I6RM4MN.mjs";
|
|
4
|
+
|
|
5
|
+
// src/browser/reverify.ts
|
|
6
|
+
var PRIOR_SESSION_STORAGE_KEY = "iqauth_prior_admin_session";
|
|
7
|
+
function enterImpersonation(manager, actorAccessToken) {
|
|
8
|
+
if (typeof window === "undefined") return;
|
|
9
|
+
const current = manager.getSnapshot().accessToken;
|
|
10
|
+
if (current) {
|
|
11
|
+
const envelope = { accessToken: current, savedAt: Date.now() };
|
|
12
|
+
try {
|
|
13
|
+
window.sessionStorage.setItem(PRIOR_SESSION_STORAGE_KEY, JSON.stringify(envelope));
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
manager.applyAccessToken(actorAccessToken);
|
|
18
|
+
}
|
|
19
|
+
function exitImpersonation(manager) {
|
|
20
|
+
if (typeof window === "undefined") return false;
|
|
21
|
+
let raw = null;
|
|
22
|
+
try {
|
|
23
|
+
raw = window.sessionStorage.getItem(PRIOR_SESSION_STORAGE_KEY);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (!raw) return false;
|
|
28
|
+
try {
|
|
29
|
+
const envelope = JSON.parse(raw);
|
|
30
|
+
if (!envelope?.accessToken) return false;
|
|
31
|
+
manager.applyAccessToken(envelope.accessToken);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
} finally {
|
|
36
|
+
try {
|
|
37
|
+
window.sessionStorage.removeItem(PRIOR_SESSION_STORAGE_KEY);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
var DEFAULT_REVERIFY_PATH = "/api/v1/auth/reverify";
|
|
43
|
+
async function reverify(manager, input, options = {}) {
|
|
44
|
+
if (input.level === "password" && !input.password) {
|
|
45
|
+
throw new IQAuthError("MISSING_PASSWORD", "password is required for level=password");
|
|
46
|
+
}
|
|
47
|
+
if (input.level === "mfa" && !input.totp) {
|
|
48
|
+
throw new IQAuthError("MISSING_CODE", "totp code is required for level=mfa");
|
|
49
|
+
}
|
|
50
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
51
|
+
const url = `${issuer}${options.path ?? DEFAULT_REVERIFY_PATH}`;
|
|
52
|
+
const res = await manager.fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify(input)
|
|
56
|
+
});
|
|
57
|
+
let payload = null;
|
|
58
|
+
try {
|
|
59
|
+
payload = await res.json();
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok || !payload?.success) {
|
|
63
|
+
const code = payload?.error?.code ?? "REVERIFICATION_FAILED";
|
|
64
|
+
const message = payload?.error?.message ?? `reverification failed (${res.status})`;
|
|
65
|
+
throw new IQAuthError(code, message);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
token: payload.data.reverificationToken,
|
|
69
|
+
level: payload.data.level,
|
|
70
|
+
expiresAt: new Date(payload.data.expiresAt)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function withReverification(manager, token) {
|
|
74
|
+
let used = false;
|
|
75
|
+
return async (input, init = {}) => {
|
|
76
|
+
if (used) throw new IQAuthError("REVERIFICATION_USED", "Reverification token already consumed");
|
|
77
|
+
used = true;
|
|
78
|
+
const headers = new Headers(init.headers);
|
|
79
|
+
headers.set("X-Reverification-Token", token);
|
|
80
|
+
return manager.fetch(input, { ...init, headers });
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
PRIOR_SESSION_STORAGE_KEY,
|
|
86
|
+
enterImpersonation,
|
|
87
|
+
exitImpersonation,
|
|
88
|
+
reverify,
|
|
89
|
+
withReverification
|
|
90
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodePublishableKey
|
|
3
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
4
|
+
|
|
5
|
+
// src/test.ts
|
|
6
|
+
import { createServer } from "http";
|
|
7
|
+
import { generateKeyPairSync, randomBytes, createPublicKey } from "crypto";
|
|
8
|
+
import jwt from "jsonwebtoken";
|
|
9
|
+
function jwkFromPublicKey(publicKey, kid) {
|
|
10
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
11
|
+
return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
|
|
12
|
+
}
|
|
13
|
+
function readBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
req.on("data", (c) => chunks.push(c));
|
|
17
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
18
|
+
req.on("error", reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function parseFormOrJson(raw, contentType) {
|
|
22
|
+
if (!raw) return {};
|
|
23
|
+
if (contentType && contentType.includes("application/json")) {
|
|
24
|
+
try {
|
|
25
|
+
const obj = JSON.parse(raw);
|
|
26
|
+
const out2 = {};
|
|
27
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
28
|
+
if (typeof v === "string") out2[k] = v;
|
|
29
|
+
else if (v != null) out2[k] = String(v);
|
|
30
|
+
}
|
|
31
|
+
return out2;
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const part of raw.split("&")) {
|
|
38
|
+
if (!part) continue;
|
|
39
|
+
const eq = part.indexOf("=");
|
|
40
|
+
const k = decodeURIComponent(eq === -1 ? part : part.slice(0, eq)).replace(/\+/g, " ");
|
|
41
|
+
const v = eq === -1 ? "" : decodeURIComponent(part.slice(eq + 1)).replace(/\+/g, " ");
|
|
42
|
+
out[k] = v;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function send(res, status, body, headers = {}) {
|
|
47
|
+
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
|
48
|
+
res.writeHead(status, {
|
|
49
|
+
"Content-Type": typeof body === "string" ? "text/plain; charset=utf-8" : "application/json; charset=utf-8",
|
|
50
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
51
|
+
...headers
|
|
52
|
+
});
|
|
53
|
+
res.end(payload);
|
|
54
|
+
}
|
|
55
|
+
async function createTestIssuer(options = {}) {
|
|
56
|
+
const host = options.host ?? "127.0.0.1";
|
|
57
|
+
const port = options.port ?? 0;
|
|
58
|
+
const tenantId = options.tenantId ?? "tenant-test";
|
|
59
|
+
const appId = options.appId ?? "app-test";
|
|
60
|
+
const kid = options.kid ?? `test-${randomBytes(6).toString("hex")}`;
|
|
61
|
+
const defaultAudience = options.defaultAudience ?? ["dispositioniq"];
|
|
62
|
+
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
|
63
|
+
modulusLength: 2048,
|
|
64
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
65
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
66
|
+
});
|
|
67
|
+
const publicKeyObj = createPublicKey(publicKey);
|
|
68
|
+
const jwk = jwkFromPublicKey(publicKeyObj, kid);
|
|
69
|
+
const pendingCodes = /* @__PURE__ */ new Map();
|
|
70
|
+
let baseUrl = "";
|
|
71
|
+
const buildToken = (opts) => {
|
|
72
|
+
const payload = {
|
|
73
|
+
sub: opts.sub ?? "test-user",
|
|
74
|
+
email: opts.email ?? "test@example.com",
|
|
75
|
+
name: opts.name ?? "Test User",
|
|
76
|
+
tenantId: opts.tenantId ?? tenantId,
|
|
77
|
+
vendorId: opts.vendorId ?? null,
|
|
78
|
+
roles: opts.roles ?? [],
|
|
79
|
+
entitlements: opts.entitlements ?? [],
|
|
80
|
+
sessionId: opts.sessionId ?? `sess-${randomBytes(4).toString("hex")}`,
|
|
81
|
+
jti: opts.jti ?? `jti-${randomBytes(4).toString("hex")}`
|
|
82
|
+
};
|
|
83
|
+
if (opts.scopeContext !== void 0) payload.scopeContext = opts.scopeContext;
|
|
84
|
+
if (opts.loginMethod !== void 0) payload.loginMethod = opts.loginMethod;
|
|
85
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
86
|
+
if (["sub", "email", "name", "tenantId", "vendorId", "roles", "entitlements", "sessionId", "jti", "scopeContext", "loginMethod", "audience", "issuer", "expiresInSeconds", "iat"].includes(k))
|
|
87
|
+
continue;
|
|
88
|
+
payload[k] = v;
|
|
89
|
+
}
|
|
90
|
+
const audience = opts.audience ?? defaultAudience;
|
|
91
|
+
const issuer = opts.issuer ?? baseUrl;
|
|
92
|
+
const expiresIn = opts.expiresInSeconds ?? 900;
|
|
93
|
+
const signOpts = {
|
|
94
|
+
algorithm: "RS256",
|
|
95
|
+
keyid: kid,
|
|
96
|
+
issuer,
|
|
97
|
+
audience
|
|
98
|
+
};
|
|
99
|
+
if (opts.iat !== void 0) {
|
|
100
|
+
payload.iat = opts.iat;
|
|
101
|
+
payload.exp = opts.iat + expiresIn;
|
|
102
|
+
} else {
|
|
103
|
+
signOpts.expiresIn = expiresIn;
|
|
104
|
+
}
|
|
105
|
+
return jwt.sign(payload, privateKey, signOpts);
|
|
106
|
+
};
|
|
107
|
+
const handler = async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(req.url || "/", baseUrl || `http://${host}`);
|
|
110
|
+
const path = url.pathname;
|
|
111
|
+
if (req.method === "OPTIONS") {
|
|
112
|
+
res.writeHead(204, {
|
|
113
|
+
"Access-Control-Allow-Origin": "*",
|
|
114
|
+
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
|
115
|
+
"Access-Control-Allow-Headers": "Authorization,Content-Type"
|
|
116
|
+
});
|
|
117
|
+
return res.end();
|
|
118
|
+
}
|
|
119
|
+
const cors = { "Access-Control-Allow-Origin": "*" };
|
|
120
|
+
if (req.method === "GET" && path === "/.well-known/openid-configuration") {
|
|
121
|
+
return send(res, 200, {
|
|
122
|
+
issuer: baseUrl,
|
|
123
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
124
|
+
authorization_endpoint: `${baseUrl}/oidc/authorize`,
|
|
125
|
+
token_endpoint: `${baseUrl}/oidc/token`,
|
|
126
|
+
userinfo_endpoint: `${baseUrl}/api/v1/auth/me`,
|
|
127
|
+
response_types_supported: ["code"],
|
|
128
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
129
|
+
subject_types_supported: ["public"],
|
|
130
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
131
|
+
code_challenge_methods_supported: ["S256"]
|
|
132
|
+
}, cors);
|
|
133
|
+
}
|
|
134
|
+
if (req.method === "GET" && path === "/.well-known/jwks.json") {
|
|
135
|
+
return send(res, 200, { keys: [jwk] }, { ...cors, "Cache-Control": "public, max-age=3600" });
|
|
136
|
+
}
|
|
137
|
+
if (req.method === "POST" && path === "/oidc/token") {
|
|
138
|
+
const raw = await readBody(req);
|
|
139
|
+
const params = parseFormOrJson(raw, req.headers["content-type"]);
|
|
140
|
+
const grant = params.grant_type;
|
|
141
|
+
if (grant === "authorization_code") {
|
|
142
|
+
const code = params.code;
|
|
143
|
+
const pending = code ? pendingCodes.get(code) : void 0;
|
|
144
|
+
if (!pending) {
|
|
145
|
+
return send(res, 400, { error: "invalid_grant", error_description: "Unknown or expired code" }, cors);
|
|
146
|
+
}
|
|
147
|
+
pendingCodes.delete(code);
|
|
148
|
+
const accessToken = buildToken(pending.claims);
|
|
149
|
+
return send(res, 200, {
|
|
150
|
+
access_token: accessToken,
|
|
151
|
+
refresh_token: pending.refreshToken,
|
|
152
|
+
id_token: accessToken,
|
|
153
|
+
token_type: "Bearer",
|
|
154
|
+
expires_in: pending.claims.expiresInSeconds ?? 900
|
|
155
|
+
}, cors);
|
|
156
|
+
}
|
|
157
|
+
if (grant === "refresh_token") {
|
|
158
|
+
const accessToken = buildToken({ sub: "test-user" });
|
|
159
|
+
return send(res, 200, {
|
|
160
|
+
access_token: accessToken,
|
|
161
|
+
refresh_token: params.refresh_token || `rt-${randomBytes(8).toString("hex")}`,
|
|
162
|
+
token_type: "Bearer",
|
|
163
|
+
expires_in: 900
|
|
164
|
+
}, cors);
|
|
165
|
+
}
|
|
166
|
+
return send(res, 400, { error: "unsupported_grant_type" }, cors);
|
|
167
|
+
}
|
|
168
|
+
if (req.method === "GET" && path === "/api/v1/auth/me") {
|
|
169
|
+
const auth = req.headers.authorization || "";
|
|
170
|
+
if (!/^Bearer /i.test(auth)) {
|
|
171
|
+
return send(res, 401, { success: false, error: { code: "TOKEN_INVALID", message: "Missing bearer" } }, cors);
|
|
172
|
+
}
|
|
173
|
+
const token = auth.slice(7).trim();
|
|
174
|
+
try {
|
|
175
|
+
const decoded = jwt.verify(token, publicKey, {
|
|
176
|
+
algorithms: ["RS256"],
|
|
177
|
+
issuer: baseUrl,
|
|
178
|
+
audience: defaultAudience
|
|
179
|
+
});
|
|
180
|
+
return send(res, 200, {
|
|
181
|
+
success: true,
|
|
182
|
+
data: {
|
|
183
|
+
id: decoded.sub,
|
|
184
|
+
email: decoded.email,
|
|
185
|
+
name: decoded.name,
|
|
186
|
+
tenantId: decoded.tenantId,
|
|
187
|
+
roles: decoded.roles,
|
|
188
|
+
entitlements: decoded.entitlements
|
|
189
|
+
}
|
|
190
|
+
}, cors);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const msg = err instanceof Error ? err.message : "verify failed";
|
|
193
|
+
return send(res, 401, { success: false, error: { code: "TOKEN_INVALID", message: msg } }, cors);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
send(res, 404, { error: "not_found", path }, cors);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = err instanceof Error ? err.message : "internal error";
|
|
199
|
+
send(res, 500, { error: "internal", message: msg });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const server = createServer((req, res) => {
|
|
203
|
+
void handler(req, res);
|
|
204
|
+
});
|
|
205
|
+
await new Promise((resolve, reject) => {
|
|
206
|
+
server.once("error", reject);
|
|
207
|
+
server.listen(port, host, () => {
|
|
208
|
+
server.off("error", reject);
|
|
209
|
+
resolve();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
const addr = server.address();
|
|
213
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : port;
|
|
214
|
+
baseUrl = `http://${host}:${boundPort}`;
|
|
215
|
+
const publishableKey = encodePublishableKey("test", {
|
|
216
|
+
iss: baseUrl,
|
|
217
|
+
appId,
|
|
218
|
+
tenantId,
|
|
219
|
+
kid
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
baseUrl,
|
|
223
|
+
publishableKey,
|
|
224
|
+
kid,
|
|
225
|
+
publicKey,
|
|
226
|
+
mintToken: (opts = {}) => buildToken(opts),
|
|
227
|
+
mintAuthCode: (opts = {}) => {
|
|
228
|
+
const code = `code-${randomBytes(12).toString("hex")}`;
|
|
229
|
+
const refreshToken = opts.refreshToken ?? `rt-${randomBytes(12).toString("hex")}`;
|
|
230
|
+
pendingCodes.set(code, { claims: opts, refreshToken });
|
|
231
|
+
return code;
|
|
232
|
+
},
|
|
233
|
+
close: () => new Promise((resolve, reject) => {
|
|
234
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
235
|
+
})
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export {
|
|
240
|
+
createTestIssuer
|
|
241
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/server/provisioningBridge.ts
|
|
2
|
+
function defaultIsUniqueViolation(err) {
|
|
3
|
+
if (!err || typeof err !== "object") return false;
|
|
4
|
+
const e = err;
|
|
5
|
+
if (e.code === "23505") return true;
|
|
6
|
+
if (typeof e.code === "string" && e.code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
7
|
+
if (typeof e.message === "string" && /unique constraint|duplicate key/i.test(e.message)) return true;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
function createProvisioningBridge(options) {
|
|
11
|
+
const { storage } = options;
|
|
12
|
+
const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
|
|
13
|
+
const roleOf = (claims) => {
|
|
14
|
+
try {
|
|
15
|
+
return options.roleMapper?.(claims) ?? null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const ensureUser = async (claims) => {
|
|
21
|
+
if (!claims?.sub) {
|
|
22
|
+
throw new Error("createProvisioningBridge: claims.sub is required");
|
|
23
|
+
}
|
|
24
|
+
const byId = await storage.findByIqAuthUserId(claims.sub);
|
|
25
|
+
if (byId) return { user: byId, claims, created: false, adopted: false };
|
|
26
|
+
if (claims.email) {
|
|
27
|
+
const byEmail = await storage.findByEmail(claims.email);
|
|
28
|
+
if (byEmail) {
|
|
29
|
+
if (storage.adoptByEmail) {
|
|
30
|
+
const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
|
|
31
|
+
return { user: adopted, claims, created: false, adopted: true };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const created = await storage.insertFromClaims(claims, roleOf(claims));
|
|
37
|
+
return { user: created, claims, created: true, adopted: false };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (!isUniqueViolation(err)) throw err;
|
|
40
|
+
const after = await storage.findByIqAuthUserId(claims.sub);
|
|
41
|
+
if (after) return { user: after, claims, created: false, adopted: false };
|
|
42
|
+
if (claims.email) {
|
|
43
|
+
const byEmail = await storage.findByEmail(claims.email);
|
|
44
|
+
if (byEmail) return { user: byEmail, claims, created: false, adopted: true };
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return { ensureUser };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
createProvisioningBridge
|
|
54
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
// src/browser/storage.ts
|
|
33
|
+
var REFRESH_COOKIE = "iqauth_rt";
|
|
34
|
+
var PKCE_STORAGE_PREFIX = "iqauth.pkce.";
|
|
35
|
+
function isBrowser() {
|
|
36
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
37
|
+
}
|
|
38
|
+
function setCookie(name, value, opts = {}) {
|
|
39
|
+
if (!isBrowser()) return;
|
|
40
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
41
|
+
parts.push(`Path=${opts.path ?? "/"}`);
|
|
42
|
+
if (opts.maxAgeSeconds !== void 0) parts.push(`Max-Age=${opts.maxAgeSeconds}`);
|
|
43
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
44
|
+
if (opts.secure ?? location.protocol === "https:") parts.push("Secure");
|
|
45
|
+
parts.push(`SameSite=${opts.sameSite ?? "lax"}`);
|
|
46
|
+
document.cookie = parts.join("; ");
|
|
47
|
+
}
|
|
48
|
+
function getCookie(name) {
|
|
49
|
+
if (!isBrowser()) return null;
|
|
50
|
+
const target = `${name}=`;
|
|
51
|
+
const segments = document.cookie ? document.cookie.split(";") : [];
|
|
52
|
+
for (const seg of segments) {
|
|
53
|
+
const trimmed = seg.trim();
|
|
54
|
+
if (trimmed.startsWith(target)) {
|
|
55
|
+
try {
|
|
56
|
+
return decodeURIComponent(trimmed.slice(target.length));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function clearCookie(name, opts = {}) {
|
|
65
|
+
setCookie(name, "", { ...opts, maxAgeSeconds: 0 });
|
|
66
|
+
}
|
|
67
|
+
function savePkce(record) {
|
|
68
|
+
if (!isBrowser()) return;
|
|
69
|
+
try {
|
|
70
|
+
sessionStorage.setItem(PKCE_STORAGE_PREFIX + record.state, JSON.stringify(record));
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function loadPkce(state) {
|
|
75
|
+
if (!isBrowser()) return null;
|
|
76
|
+
try {
|
|
77
|
+
const raw = sessionStorage.getItem(PKCE_STORAGE_PREFIX + state);
|
|
78
|
+
if (!raw) return null;
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function clearPkce(state) {
|
|
85
|
+
if (!isBrowser()) return;
|
|
86
|
+
try {
|
|
87
|
+
sessionStorage.removeItem(PKCE_STORAGE_PREFIX + state);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/browser/signIn.ts
|
|
93
|
+
var DEFAULT_SIGN_IN_PATH = "/sign-in";
|
|
94
|
+
var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
|
|
95
|
+
var DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
|
|
96
|
+
var DEFAULT_TOKEN_PATH = "/oidc/token";
|
|
97
|
+
var DEFAULT_CALLBACK_PATH = "/auth/callback";
|
|
98
|
+
function defaultRedirectUri() {
|
|
99
|
+
if (typeof window === "undefined") {
|
|
100
|
+
throw new Error("redirectToSignIn requires a browser environment (window)");
|
|
101
|
+
}
|
|
102
|
+
return `${window.location.origin}${DEFAULT_CALLBACK_PATH}`;
|
|
103
|
+
}
|
|
104
|
+
function defaultReturnTo() {
|
|
105
|
+
if (typeof window === "undefined") return "/";
|
|
106
|
+
return window.location.href;
|
|
107
|
+
}
|
|
108
|
+
async function buildSignInUrl(manager, opts = {}) {
|
|
109
|
+
const pkce = await createPkcePair();
|
|
110
|
+
const redirectUri = opts.redirectUri ?? defaultRedirectUri();
|
|
111
|
+
const returnTo = opts.returnTo ?? defaultReturnTo();
|
|
112
|
+
savePkce({
|
|
113
|
+
codeVerifier: pkce.codeVerifier,
|
|
114
|
+
state: pkce.state,
|
|
115
|
+
nonce: pkce.nonce,
|
|
116
|
+
redirectUri,
|
|
117
|
+
appKey: manager.publishableKey.raw,
|
|
118
|
+
returnTo,
|
|
119
|
+
createdAt: Date.now()
|
|
120
|
+
});
|
|
121
|
+
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
|
|
122
|
+
url.searchParams.set("response_type", "code");
|
|
123
|
+
url.searchParams.set("app", manager.appKey);
|
|
124
|
+
url.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
125
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
126
|
+
url.searchParams.set("state", pkce.state);
|
|
127
|
+
url.searchParams.set("nonce", pkce.nonce);
|
|
128
|
+
url.searchParams.set("code_challenge", pkce.codeChallenge);
|
|
129
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
130
|
+
url.searchParams.set("scope", opts.scope ?? "openid profile email");
|
|
131
|
+
url.searchParams.set("return_to", returnTo);
|
|
132
|
+
if (opts.prompt) url.searchParams.set("prompt", opts.prompt);
|
|
133
|
+
return url.toString();
|
|
134
|
+
}
|
|
135
|
+
async function redirectToSignIn(manager, opts = {}) {
|
|
136
|
+
const url = await buildSignInUrl(manager, opts);
|
|
137
|
+
if (typeof window === "undefined") {
|
|
138
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
139
|
+
}
|
|
140
|
+
window.location.assign(url);
|
|
141
|
+
}
|
|
142
|
+
async function signIn(manager, opts = {}) {
|
|
143
|
+
return redirectToSignIn(manager, opts);
|
|
144
|
+
}
|
|
145
|
+
async function handleAuthCallback(manager, options = {}) {
|
|
146
|
+
const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
147
|
+
const code = url.searchParams.get("code");
|
|
148
|
+
const state = url.searchParams.get("state");
|
|
149
|
+
const errorParam = url.searchParams.get("error");
|
|
150
|
+
if (errorParam) {
|
|
151
|
+
return { ok: false, returnTo: "/", error: errorParam };
|
|
152
|
+
}
|
|
153
|
+
if (!code || !state) {
|
|
154
|
+
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
155
|
+
}
|
|
156
|
+
const record = loadPkce(state);
|
|
157
|
+
if (!record) {
|
|
158
|
+
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
159
|
+
}
|
|
160
|
+
clearPkce(state);
|
|
161
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
162
|
+
if (!fetchImpl) {
|
|
163
|
+
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
164
|
+
}
|
|
165
|
+
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
166
|
+
const res = await fetchImpl(tokenUrl, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
credentials: "include",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
grant_type: "authorization_code",
|
|
172
|
+
code,
|
|
173
|
+
redirect_uri: record.redirectUri,
|
|
174
|
+
client_id: manager.appKey,
|
|
175
|
+
code_verifier: record.codeVerifier
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
const body = await res.json().catch(() => ({}));
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
181
|
+
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
182
|
+
}
|
|
183
|
+
const tokens = body;
|
|
184
|
+
if (!tokens.access_token) {
|
|
185
|
+
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
186
|
+
}
|
|
187
|
+
if (tokens.refresh_token) {
|
|
188
|
+
const cookieName = options.cookieNames?.refresh ?? manager.refreshCookie ?? REFRESH_COOKIE;
|
|
189
|
+
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
190
|
+
}
|
|
191
|
+
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
192
|
+
return { ok: true, returnTo: record.returnTo };
|
|
193
|
+
}
|
|
194
|
+
async function signOut(manager, opts = {}) {
|
|
195
|
+
if (!opts.localOnly) {
|
|
196
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
197
|
+
try {
|
|
198
|
+
const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
199
|
+
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (opts.endSsoSession !== false) {
|
|
203
|
+
try {
|
|
204
|
+
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
credentials: "include"
|
|
207
|
+
}).catch(() => void 0);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
clearCookie(manager.refreshCookie ?? REFRESH_COOKIE);
|
|
213
|
+
manager.signOutLocal();
|
|
214
|
+
if (opts.returnTo && typeof window !== "undefined") {
|
|
215
|
+
window.location.assign(opts.returnTo);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export {
|
|
220
|
+
REFRESH_COOKIE,
|
|
221
|
+
setCookie,
|
|
222
|
+
getCookie,
|
|
223
|
+
clearCookie,
|
|
224
|
+
randomUrlSafe,
|
|
225
|
+
s256Challenge,
|
|
226
|
+
createPkcePair,
|
|
227
|
+
buildSignInUrl,
|
|
228
|
+
redirectToSignIn,
|
|
229
|
+
signIn,
|
|
230
|
+
handleAuthCallback,
|
|
231
|
+
signOut
|
|
232
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/webhooks.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
var WebhookSignatureError = class extends Error {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "WebhookSignatureError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function toBuffer(p) {
|
|
11
|
+
if (typeof p === "string") return Buffer.from(p, "utf8");
|
|
12
|
+
if (Buffer.isBuffer(p)) return p;
|
|
13
|
+
return Buffer.from(p);
|
|
14
|
+
}
|
|
15
|
+
function parseHeader(header) {
|
|
16
|
+
let t = NaN;
|
|
17
|
+
const v1 = [];
|
|
18
|
+
for (const part of header.split(",")) {
|
|
19
|
+
const [k, v] = part.split("=", 2);
|
|
20
|
+
if (!k || v === void 0) continue;
|
|
21
|
+
const key = k.trim();
|
|
22
|
+
const value = v.trim();
|
|
23
|
+
if (key === "t") t = Number(value);
|
|
24
|
+
else if (key === "v1") v1.push(value);
|
|
25
|
+
}
|
|
26
|
+
return { t, v1 };
|
|
27
|
+
}
|
|
28
|
+
function timingSafeEqualHex(a, b) {
|
|
29
|
+
if (a.length !== b.length) return false;
|
|
30
|
+
try {
|
|
31
|
+
return crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function verifyWebhookSignature(opts) {
|
|
37
|
+
const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
|
|
38
|
+
if (!headerRaw || typeof headerRaw !== "string") {
|
|
39
|
+
throw new WebhookSignatureError("MISSING_HEADER", "Missing X-IQAuth-Signature header");
|
|
40
|
+
}
|
|
41
|
+
if (!opts.secret) {
|
|
42
|
+
throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
|
|
43
|
+
}
|
|
44
|
+
const { t, v1 } = parseHeader(headerRaw);
|
|
45
|
+
if (!Number.isFinite(t) || v1.length === 0) {
|
|
46
|
+
throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
|
|
47
|
+
}
|
|
48
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
49
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
50
|
+
if (Math.abs(now - t) > tolerance) {
|
|
51
|
+
throw new WebhookSignatureError(
|
|
52
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
53
|
+
`Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const body = toBuffer(opts.payload);
|
|
57
|
+
const expected = crypto.createHmac("sha256", opts.secret).update(`${t}.`).update(body).digest("hex");
|
|
58
|
+
const matched = v1.some((sig) => timingSafeEqualHex(sig, expected));
|
|
59
|
+
if (!matched) {
|
|
60
|
+
throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
function isValidWebhookSignature(opts) {
|
|
71
|
+
try {
|
|
72
|
+
verifyWebhookSignature(opts);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
WebhookSignatureError,
|
|
81
|
+
verifyWebhookSignature,
|
|
82
|
+
isValidWebhookSignature
|
|
83
|
+
};
|