@holo-js/security 0.3.8 → 0.3.11
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.
|
@@ -1119,6 +1119,109 @@ var securityStoreInternals = {
|
|
|
1119
1119
|
normalizeStoreConfig
|
|
1120
1120
|
};
|
|
1121
1121
|
|
|
1122
|
+
// src/signed-tokens.ts
|
|
1123
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1124
|
+
var MAX_SIGNED_TOKEN_DEPTH = 32;
|
|
1125
|
+
var MAX_SIGNED_TOKEN_BODY_BYTES = 64 * 1024;
|
|
1126
|
+
var MAX_SIGNED_TOKEN_CHARACTERS = Math.ceil(MAX_SIGNED_TOKEN_BODY_BYTES * 4 / 3) + 44;
|
|
1127
|
+
function signingKey() {
|
|
1128
|
+
const key = getSecurityRuntime().csrfSigningKey?.trim();
|
|
1129
|
+
if (!key) {
|
|
1130
|
+
throw new Error("[@holo-js/security] Signed tokens require an application signing key.");
|
|
1131
|
+
}
|
|
1132
|
+
return key;
|
|
1133
|
+
}
|
|
1134
|
+
function normalizePurpose(purpose) {
|
|
1135
|
+
const normalized = purpose.trim();
|
|
1136
|
+
if (!normalized) {
|
|
1137
|
+
throw new TypeError("[@holo-js/security] Signed token purpose must be a non-empty string.");
|
|
1138
|
+
}
|
|
1139
|
+
return normalized;
|
|
1140
|
+
}
|
|
1141
|
+
function isSignedTokenValue(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1142
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
1143
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1144
|
+
if (!value || typeof value !== "object") return false;
|
|
1145
|
+
if (depth >= MAX_SIGNED_TOKEN_DEPTH || ancestors.has(value)) return false;
|
|
1146
|
+
ancestors.add(value);
|
|
1147
|
+
try {
|
|
1148
|
+
if (Array.isArray(value)) {
|
|
1149
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
1150
|
+
if (ownKeys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/u.test(key)))) return false;
|
|
1151
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1152
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1153
|
+
if (!descriptor || !("value" in descriptor) || !isSignedTokenValue(descriptor.value, ancestors, depth + 1)) return false;
|
|
1154
|
+
}
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1158
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
1159
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1160
|
+
if (typeof key !== "string") return false;
|
|
1161
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1162
|
+
if (!descriptor || !("value" in descriptor) || !isSignedTokenValue(descriptor.value, ancestors, depth + 1)) return false;
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
} finally {
|
|
1166
|
+
ancestors.delete(value);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function assertPayload(payload) {
|
|
1170
|
+
if (!isSignedTokenValue(payload) || Array.isArray(payload) || payload === null) {
|
|
1171
|
+
throw new TypeError("[@holo-js/security] Signed token payload must contain only JSON-safe values.");
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function signature(body, purpose) {
|
|
1175
|
+
return createHmac2("sha256", signingKey()).update("holo-signed-token-v1\0").update(purpose).update("\0").update(body).digest();
|
|
1176
|
+
}
|
|
1177
|
+
function decodeEnvelope(body) {
|
|
1178
|
+
try {
|
|
1179
|
+
const value = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
1180
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1181
|
+
const envelope = value;
|
|
1182
|
+
if (envelope.version !== 1 || !Number.isSafeInteger(envelope.expiresAt)) return null;
|
|
1183
|
+
if (!isSignedTokenValue(envelope.payload) || Array.isArray(envelope.payload) || envelope.payload === null) return null;
|
|
1184
|
+
return envelope;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function createSignedToken(payload, options) {
|
|
1190
|
+
assertPayload(payload);
|
|
1191
|
+
const purpose = normalizePurpose(options.purpose);
|
|
1192
|
+
const expiresAt = options.expiresAt.getTime();
|
|
1193
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now()) {
|
|
1194
|
+
throw new TypeError("[@holo-js/security] Signed token expiry must be a valid future date.");
|
|
1195
|
+
}
|
|
1196
|
+
const encodedEnvelope = JSON.stringify({
|
|
1197
|
+
expiresAt,
|
|
1198
|
+
payload,
|
|
1199
|
+
version: 1
|
|
1200
|
+
});
|
|
1201
|
+
if (Buffer.byteLength(encodedEnvelope, "utf8") > MAX_SIGNED_TOKEN_BODY_BYTES) {
|
|
1202
|
+
throw new TypeError("[@holo-js/security] Signed token payload exceeds 64 KiB.");
|
|
1203
|
+
}
|
|
1204
|
+
const body = Buffer.from(encodedEnvelope).toString("base64url");
|
|
1205
|
+
return `${body}.${signature(body, purpose).toString("base64url")}`;
|
|
1206
|
+
}
|
|
1207
|
+
function verifySignedToken(token2, options) {
|
|
1208
|
+
if (token2.length > MAX_SIGNED_TOKEN_CHARACTERS) return null;
|
|
1209
|
+
const purpose = normalizePurpose(options.purpose);
|
|
1210
|
+
const separator = token2.lastIndexOf(".");
|
|
1211
|
+
if (separator <= 0 || separator === token2.length - 1) return null;
|
|
1212
|
+
const body = token2.slice(0, separator);
|
|
1213
|
+
const encodedSignature = token2.slice(separator + 1);
|
|
1214
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(encodedSignature)) return null;
|
|
1215
|
+
const providedSignature = Buffer.from(encodedSignature, "base64url");
|
|
1216
|
+
if (providedSignature.toString("base64url") !== encodedSignature) return null;
|
|
1217
|
+
const expectedSignature = signature(body, purpose);
|
|
1218
|
+
if (providedSignature.length !== expectedSignature.length || !timingSafeEqual2(providedSignature, expectedSignature)) return null;
|
|
1219
|
+
const envelope = decodeEnvelope(body);
|
|
1220
|
+
const now = options.now?.getTime() ?? Date.now();
|
|
1221
|
+
if (!envelope || !Number.isFinite(now) || envelope.expiresAt <= now) return null;
|
|
1222
|
+
return envelope.payload;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1122
1225
|
// src/index.ts
|
|
1123
1226
|
var security = Object.freeze({
|
|
1124
1227
|
configureSecurityRuntime,
|
|
@@ -1132,7 +1235,9 @@ var security = Object.freeze({
|
|
|
1132
1235
|
rateLimit,
|
|
1133
1236
|
clearRateLimit,
|
|
1134
1237
|
limit,
|
|
1135
|
-
ip
|
|
1238
|
+
ip,
|
|
1239
|
+
createSignedToken,
|
|
1240
|
+
verifySignedToken
|
|
1136
1241
|
});
|
|
1137
1242
|
var src_default = security;
|
|
1138
1243
|
|
|
@@ -1169,5 +1274,7 @@ export {
|
|
|
1169
1274
|
redisRateLimitDriverInternals,
|
|
1170
1275
|
createRateLimitStoreFromConfig,
|
|
1171
1276
|
securityStoreInternals,
|
|
1277
|
+
createSignedToken,
|
|
1278
|
+
verifySignedToken,
|
|
1172
1279
|
src_default
|
|
1173
1280
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -210,6 +210,21 @@ declare const securityRuntimeInternals: {
|
|
|
210
210
|
getSecurityRuntimeState: typeof getSecurityRuntimeState;
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
+
type SecuritySignedTokenPrimitive = boolean | number | string | null;
|
|
214
|
+
type SecuritySignedTokenValue = SecuritySignedTokenPrimitive | readonly SecuritySignedTokenValue[] | {
|
|
215
|
+
readonly [key: string]: SecuritySignedTokenValue;
|
|
216
|
+
};
|
|
217
|
+
type SecuritySignedTokenPayload = Readonly<Record<string, SecuritySignedTokenValue>>;
|
|
218
|
+
interface SecuritySignedTokenOptions {
|
|
219
|
+
readonly expiresAt: Date;
|
|
220
|
+
readonly purpose: string;
|
|
221
|
+
}
|
|
222
|
+
declare function createSignedToken<TPayload extends SecuritySignedTokenPayload>(payload: TPayload, options: SecuritySignedTokenOptions): string;
|
|
223
|
+
declare function verifySignedToken<TPayload extends SecuritySignedTokenPayload>(token: string, options: {
|
|
224
|
+
readonly now?: Date;
|
|
225
|
+
readonly purpose: string;
|
|
226
|
+
}): TPayload | null;
|
|
227
|
+
|
|
213
228
|
declare const security: Readonly<{
|
|
214
229
|
configureSecurityRuntime: typeof configureSecurityRuntime;
|
|
215
230
|
getSecurityRuntime: typeof getSecurityRuntime;
|
|
@@ -246,6 +261,8 @@ declare const security: Readonly<{
|
|
|
246
261
|
};
|
|
247
262
|
}>;
|
|
248
263
|
ip: typeof ip;
|
|
264
|
+
createSignedToken: typeof createSignedToken;
|
|
265
|
+
verifySignedToken: typeof verifySignedToken;
|
|
249
266
|
}>;
|
|
250
267
|
|
|
251
|
-
export { HoloSecurityConfig, NormalizedHoloCorsConfig, NormalizedHoloSecurityConfig, NormalizedSecurityLimiterConfig, SecurityClearRateLimitOptions, SecurityCsrfField, SecurityCsrfInput, SecurityLimiterConfig, SecurityProtectOptions, SecurityRateLimitCallOptions, SecurityRateLimitHitResult, SecurityRateLimitKeyResolver, SecurityRateLimitRedisDriverAdapter, SecurityRateLimitStore, SecurityRateLimitStoreFactoryOptions, SecurityRuntimeBindings, SecurityRuntimeFacade, SecurityRuntimeNotConfiguredError, apply as applyCors, clearRateLimit, configureSecurityRuntime, cors, corsInternals, headers as createCorsHeaders, preflight as createCorsPreflightResponse, cookie as createCsrfCookie, field as createCsrfField, input as createCsrfInput, token as createCsrfToken, createFileRateLimitStore, createMemoryRateLimitStore, createRateLimitStoreFromConfig, createRedisRateLimitStore, csrf, csrfInternals, security as default, defaultRateLimitKey, fileRateLimitDriverInternals, getSecurityRuntime, getSecurityRuntimeBindings, ip, isSecureRequest, memoryRateLimitDriverInternals, protect, rateLimit, rateLimitInternals, redisRateLimitDriverInternals, resetSecurityRuntime, securityRuntimeInternals, securityStoreInternals, verify as verifyCsrfRequest };
|
|
268
|
+
export { HoloSecurityConfig, NormalizedHoloCorsConfig, NormalizedHoloSecurityConfig, NormalizedSecurityLimiterConfig, SecurityClearRateLimitOptions, SecurityCsrfField, SecurityCsrfInput, SecurityLimiterConfig, SecurityProtectOptions, SecurityRateLimitCallOptions, SecurityRateLimitHitResult, SecurityRateLimitKeyResolver, SecurityRateLimitRedisDriverAdapter, SecurityRateLimitStore, SecurityRateLimitStoreFactoryOptions, SecurityRuntimeBindings, SecurityRuntimeFacade, SecurityRuntimeNotConfiguredError, type SecuritySignedTokenOptions, type SecuritySignedTokenPayload, type SecuritySignedTokenPrimitive, type SecuritySignedTokenValue, apply as applyCors, clearRateLimit, configureSecurityRuntime, cors, corsInternals, headers as createCorsHeaders, preflight as createCorsPreflightResponse, cookie as createCsrfCookie, field as createCsrfField, input as createCsrfInput, token as createCsrfToken, createFileRateLimitStore, createMemoryRateLimitStore, createRateLimitStoreFromConfig, createRedisRateLimitStore, createSignedToken, csrf, csrfInternals, security as default, defaultRateLimitKey, fileRateLimitDriverInternals, getSecurityRuntime, getSecurityRuntimeBindings, ip, isSecureRequest, memoryRateLimitDriverInternals, protect, rateLimit, rateLimitInternals, redisRateLimitDriverInternals, resetSecurityRuntime, securityRuntimeInternals, securityStoreInternals, verify as verifyCsrfRequest, verifySignedToken };
|
package/dist/index.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createMemoryRateLimitStore,
|
|
11
11
|
createRateLimitStoreFromConfig,
|
|
12
12
|
createRedisRateLimitStore,
|
|
13
|
+
createSignedToken,
|
|
13
14
|
csrf,
|
|
14
15
|
csrfInternals,
|
|
15
16
|
defaultRateLimitKey,
|
|
@@ -31,8 +32,9 @@ import {
|
|
|
31
32
|
securityStoreInternals,
|
|
32
33
|
src_default,
|
|
33
34
|
token,
|
|
34
|
-
verify
|
|
35
|
-
|
|
35
|
+
verify,
|
|
36
|
+
verifySignedToken
|
|
37
|
+
} from "./chunk-YIBZOYSR.mjs";
|
|
36
38
|
import {
|
|
37
39
|
SecurityCsrfError,
|
|
38
40
|
SecurityRateLimitError,
|
|
@@ -75,6 +77,7 @@ export {
|
|
|
75
77
|
createRateLimitStoreFromConfig,
|
|
76
78
|
createRedisRateLimitStore,
|
|
77
79
|
createRedisRateLimitStoreConfig,
|
|
80
|
+
createSignedToken,
|
|
78
81
|
csrf,
|
|
79
82
|
csrfInternals,
|
|
80
83
|
src_default as default,
|
|
@@ -102,5 +105,6 @@ export {
|
|
|
102
105
|
securityInternals,
|
|
103
106
|
securityRuntimeInternals,
|
|
104
107
|
securityStoreInternals,
|
|
105
|
-
verify as verifyCsrfRequest
|
|
108
|
+
verify as verifyCsrfRequest,
|
|
109
|
+
verifySignedToken
|
|
106
110
|
};
|
package/dist/next/server.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
getSecurityRuntime,
|
|
5
5
|
isSecureRequest,
|
|
6
6
|
protect
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-YIBZOYSR.mjs";
|
|
8
8
|
import {
|
|
9
9
|
SECURITY_CLIENT_CONFIG_COOKIE,
|
|
10
10
|
createSecurityClientConfig,
|
|
@@ -47,7 +47,7 @@ async function issueCsrfCookie(request) {
|
|
|
47
47
|
if (!shouldIssueCsrfToken && !shouldIssueClientConfig) {
|
|
48
48
|
return void 0;
|
|
49
49
|
}
|
|
50
|
-
const { NextResponse } = await import("next/server");
|
|
50
|
+
const { NextResponse } = await import("next/server.js");
|
|
51
51
|
const response = NextResponse.next();
|
|
52
52
|
const cookieOptions = {
|
|
53
53
|
httpOnly: false,
|
package/dist/nuxt/server.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/security",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Holo-JS Framework - CSRF and rate-limit contracts, config, and runtime seams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"test": "vitest --run --coverage"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@holo-js/config": "^0.3.
|
|
62
|
-
"@holo-js/kernel": "^0.3.
|
|
61
|
+
"@holo-js/config": "^0.3.11",
|
|
62
|
+
"@holo-js/kernel": "^0.3.11"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"h3": "^1.15.11",
|