@holo-js/security 0.3.7 → 0.3.10

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
- } from "./chunk-NKKBOLDI.mjs";
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
  };
@@ -4,7 +4,7 @@ import {
4
4
  getSecurityRuntime,
5
5
  isSecureRequest,
6
6
  protect
7
- } from "../chunk-NKKBOLDI.mjs";
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,
@@ -4,7 +4,7 @@ import {
4
4
  getSecurityRuntime,
5
5
  isSecureRequest,
6
6
  protect
7
- } from "../chunk-NKKBOLDI.mjs";
7
+ } from "../chunk-YIBZOYSR.mjs";
8
8
  import {
9
9
  SECURITY_CLIENT_CONFIG_COOKIE,
10
10
  createSecurityClientConfig,
@@ -3,7 +3,7 @@ import {
3
3
  getSecurityRuntime,
4
4
  isSecureRequest,
5
5
  protect
6
- } from "../chunk-NKKBOLDI.mjs";
6
+ } from "../chunk-YIBZOYSR.mjs";
7
7
  import {
8
8
  SECURITY_CLIENT_CONFIG_COOKIE,
9
9
  createSecurityClientConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/security",
3
- "version": "0.3.7",
3
+ "version": "0.3.10",
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.7",
62
- "@holo-js/kernel": "^0.3.7"
61
+ "@holo-js/config": "^0.3.10",
62
+ "@holo-js/kernel": "^0.3.10"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "h3": "^1.15.11",