namespace-guard 0.8.1 → 0.8.2

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 CHANGED
@@ -648,7 +648,8 @@ Enable in-memory caching to reduce database calls during rapid checks (e.g., liv
648
648
  const guard = createNamespaceGuard({
649
649
  sources: [/* ... */],
650
650
  cache: {
651
- ttl: 5000, // milliseconds (default: 5000)
651
+ ttl: 5000, // milliseconds (default: 5000)
652
+ maxSize: 1000, // max cached entries before LRU eviction (default: 1000)
652
653
  },
653
654
  }, adapter);
654
655
 
@@ -3,32 +3,6 @@ import { NamespaceAdapter } from '../index.mjs';
3
3
  type QueryExecutor = (sql: string, params: unknown[]) => Promise<{
4
4
  rows: Record<string, unknown>[];
5
5
  }>;
6
- /**
7
- * Create a namespace adapter for raw SQL queries
8
- *
9
- * Works with PostgreSQL-compatible clients that use $1-style parameter placeholders (pg).
10
- * For MySQL or SQLite, wrap the executor to translate parameter syntax.
11
- *
12
- * @example
13
- * ```ts
14
- * import { Pool } from "pg";
15
- * import { createNamespaceGuard } from "namespace-guard";
16
- * import { createRawAdapter } from "namespace-guard/adapters/raw";
17
- *
18
- * const pool = new Pool();
19
- *
20
- * const guard = createNamespaceGuard(
21
- * {
22
- * reserved: ["admin", "api", "settings"],
23
- * sources: [
24
- * { name: "users", column: "handle", scopeKey: "id" },
25
- * { name: "organizations", column: "slug", scopeKey: "id" },
26
- * ],
27
- * },
28
- * createRawAdapter((sql, params) => pool.query(sql, params))
29
- * );
30
- * ```
31
- */
32
6
  declare function createRawAdapter(execute: QueryExecutor): NamespaceAdapter;
33
7
 
34
8
  export { createRawAdapter };
@@ -3,32 +3,6 @@ import { NamespaceAdapter } from '../index.js';
3
3
  type QueryExecutor = (sql: string, params: unknown[]) => Promise<{
4
4
  rows: Record<string, unknown>[];
5
5
  }>;
6
- /**
7
- * Create a namespace adapter for raw SQL queries
8
- *
9
- * Works with PostgreSQL-compatible clients that use $1-style parameter placeholders (pg).
10
- * For MySQL or SQLite, wrap the executor to translate parameter syntax.
11
- *
12
- * @example
13
- * ```ts
14
- * import { Pool } from "pg";
15
- * import { createNamespaceGuard } from "namespace-guard";
16
- * import { createRawAdapter } from "namespace-guard/adapters/raw";
17
- *
18
- * const pool = new Pool();
19
- *
20
- * const guard = createNamespaceGuard(
21
- * {
22
- * reserved: ["admin", "api", "settings"],
23
- * sources: [
24
- * { name: "users", column: "handle", scopeKey: "id" },
25
- * { name: "organizations", column: "slug", scopeKey: "id" },
26
- * ],
27
- * },
28
- * createRawAdapter((sql, params) => pool.query(sql, params))
29
- * );
30
- * ```
31
- */
32
6
  declare function createRawAdapter(execute: QueryExecutor): NamespaceAdapter;
33
7
 
34
8
  export { createRawAdapter };
@@ -23,10 +23,20 @@ __export(raw_exports, {
23
23
  createRawAdapter: () => createRawAdapter
24
24
  });
25
25
  module.exports = __toCommonJS(raw_exports);
26
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
27
+ function assertSafeIdentifier(name, label) {
28
+ if (!SAFE_IDENTIFIER.test(name)) {
29
+ throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
30
+ }
31
+ }
26
32
  function createRawAdapter(execute) {
27
33
  return {
28
34
  async findOne(source, value, options) {
29
35
  const idColumn = source.idColumn ?? "id";
36
+ assertSafeIdentifier(source.name, "table name");
37
+ assertSafeIdentifier(source.column, "column name");
38
+ assertSafeIdentifier(idColumn, "id column name");
39
+ if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
30
40
  const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
31
41
  const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
32
42
  const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
@@ -1,8 +1,18 @@
1
1
  // src/adapters/raw.ts
2
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3
+ function assertSafeIdentifier(name, label) {
4
+ if (!SAFE_IDENTIFIER.test(name)) {
5
+ throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
6
+ }
7
+ }
2
8
  function createRawAdapter(execute) {
3
9
  return {
4
10
  async findOne(source, value, options) {
5
11
  const idColumn = source.idColumn ?? "id";
12
+ assertSafeIdentifier(source.name, "table name");
13
+ assertSafeIdentifier(source.column, "column name");
14
+ assertSafeIdentifier(idColumn, "id column name");
15
+ if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
6
16
  const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
7
17
  const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
8
18
  const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
package/dist/cli.js CHANGED
@@ -144,14 +144,12 @@ function createScrambleStrategy(_pattern) {
144
144
  const candidates = [];
145
145
  const chars = identifier.split("");
146
146
  for (let i = 0; i < chars.length - 1; i++) {
147
- if (chars[i] !== chars[i + 1]) {
148
- const swapped = [...chars];
149
- [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
150
- const candidate = swapped.join("");
151
- if (candidate !== identifier && !seen.has(candidate)) {
152
- seen.add(candidate);
153
- candidates.push(candidate);
154
- }
147
+ const swapped = [...chars];
148
+ [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
149
+ const candidate = swapped.join("");
150
+ if (candidate !== identifier && !seen.has(candidate)) {
151
+ seen.add(candidate);
152
+ candidates.push(candidate);
155
153
  }
156
154
  }
157
155
  return candidates;
@@ -298,7 +296,7 @@ function createNamespaceGuard(config, adapter) {
298
296
  const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
299
297
  const cacheEnabled = !!config.cache;
300
298
  const cacheTtl = config.cache?.ttl ?? 5e3;
301
- const cacheMaxSize = 1e3;
299
+ const cacheMaxSize = config.cache?.maxSize ?? 1e3;
302
300
  const cacheMap = /* @__PURE__ */ new Map();
303
301
  let cacheHits = 0;
304
302
  let cacheMisses = 0;
@@ -488,10 +486,20 @@ function createNamespaceGuard(config, adapter) {
488
486
  }
489
487
 
490
488
  // src/adapters/raw.ts
489
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
490
+ function assertSafeIdentifier(name, label) {
491
+ if (!SAFE_IDENTIFIER.test(name)) {
492
+ throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
493
+ }
494
+ }
491
495
  function createRawAdapter(execute) {
492
496
  return {
493
497
  async findOne(source, value, options) {
494
498
  const idColumn = source.idColumn ?? "id";
499
+ assertSafeIdentifier(source.name, "table name");
500
+ assertSafeIdentifier(source.column, "column name");
501
+ assertSafeIdentifier(idColumn, "id column name");
502
+ if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
495
503
  const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
496
504
  const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
497
505
  const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
@@ -574,6 +582,10 @@ async function createDatabaseAdapter(url) {
574
582
  process.exit(1);
575
583
  }
576
584
  const Pool = pg.default?.Pool ?? pg.Pool;
585
+ if (!Pool) {
586
+ console.error("Could not find Pool export from pg package. Check your pg version.");
587
+ process.exit(1);
588
+ }
577
589
  const pool = new Pool({ connectionString: url });
578
590
  return {
579
591
  adapter: createRawAdapter((sql, params) => pool.query(sql, params)),
package/dist/cli.mjs CHANGED
@@ -121,14 +121,12 @@ function createScrambleStrategy(_pattern) {
121
121
  const candidates = [];
122
122
  const chars = identifier.split("");
123
123
  for (let i = 0; i < chars.length - 1; i++) {
124
- if (chars[i] !== chars[i + 1]) {
125
- const swapped = [...chars];
126
- [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
127
- const candidate = swapped.join("");
128
- if (candidate !== identifier && !seen.has(candidate)) {
129
- seen.add(candidate);
130
- candidates.push(candidate);
131
- }
124
+ const swapped = [...chars];
125
+ [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
126
+ const candidate = swapped.join("");
127
+ if (candidate !== identifier && !seen.has(candidate)) {
128
+ seen.add(candidate);
129
+ candidates.push(candidate);
132
130
  }
133
131
  }
134
132
  return candidates;
@@ -275,7 +273,7 @@ function createNamespaceGuard(config, adapter) {
275
273
  const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
276
274
  const cacheEnabled = !!config.cache;
277
275
  const cacheTtl = config.cache?.ttl ?? 5e3;
278
- const cacheMaxSize = 1e3;
276
+ const cacheMaxSize = config.cache?.maxSize ?? 1e3;
279
277
  const cacheMap = /* @__PURE__ */ new Map();
280
278
  let cacheHits = 0;
281
279
  let cacheMisses = 0;
@@ -465,10 +463,20 @@ function createNamespaceGuard(config, adapter) {
465
463
  }
466
464
 
467
465
  // src/adapters/raw.ts
466
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
467
+ function assertSafeIdentifier(name, label) {
468
+ if (!SAFE_IDENTIFIER.test(name)) {
469
+ throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
470
+ }
471
+ }
468
472
  function createRawAdapter(execute) {
469
473
  return {
470
474
  async findOne(source, value, options) {
471
475
  const idColumn = source.idColumn ?? "id";
476
+ assertSafeIdentifier(source.name, "table name");
477
+ assertSafeIdentifier(source.column, "column name");
478
+ assertSafeIdentifier(idColumn, "id column name");
479
+ if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
472
480
  const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
473
481
  const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
474
482
  const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
@@ -551,6 +559,10 @@ async function createDatabaseAdapter(url) {
551
559
  process.exit(1);
552
560
  }
553
561
  const Pool = pg.default?.Pool ?? pg.Pool;
562
+ if (!Pool) {
563
+ console.error("Could not find Pool export from pg package. Check your pg version.");
564
+ process.exit(1);
565
+ }
554
566
  const pool = new Pool({ connectionString: url });
555
567
  return {
556
568
  adapter: createRawAdapter((sql, params) => pool.query(sql, params)),
package/dist/index.d.mts CHANGED
@@ -53,6 +53,8 @@ type NamespaceConfig = {
53
53
  cache?: {
54
54
  /** Time-to-live in milliseconds (default: 5000) */
55
55
  ttl?: number;
56
+ /** Maximum number of cached entries before LRU eviction (default: 1000) */
57
+ maxSize?: number;
56
58
  };
57
59
  };
58
60
  /** Options passed to adapter `findOne` calls. */
package/dist/index.d.ts CHANGED
@@ -53,6 +53,8 @@ type NamespaceConfig = {
53
53
  cache?: {
54
54
  /** Time-to-live in milliseconds (default: 5000) */
55
55
  ttl?: number;
56
+ /** Maximum number of cached entries before LRU eviction (default: 1000) */
57
+ maxSize?: number;
56
58
  };
57
59
  };
58
60
  /** Options passed to adapter `findOne` calls. */
package/dist/index.js CHANGED
@@ -143,14 +143,12 @@ function createScrambleStrategy(_pattern) {
143
143
  const candidates = [];
144
144
  const chars = identifier.split("");
145
145
  for (let i = 0; i < chars.length - 1; i++) {
146
- if (chars[i] !== chars[i + 1]) {
147
- const swapped = [...chars];
148
- [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
149
- const candidate = swapped.join("");
150
- if (candidate !== identifier && !seen.has(candidate)) {
151
- seen.add(candidate);
152
- candidates.push(candidate);
153
- }
146
+ const swapped = [...chars];
147
+ [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
148
+ const candidate = swapped.join("");
149
+ if (candidate !== identifier && !seen.has(candidate)) {
150
+ seen.add(candidate);
151
+ candidates.push(candidate);
154
152
  }
155
153
  }
156
154
  return candidates;
@@ -971,7 +969,9 @@ function createHomoglyphValidator(options) {
971
969
  Object.assign(map, options.additionalMappings);
972
970
  }
973
971
  const confusableChars = Object.keys(map);
974
- const confusableRegex = confusableChars.length > 0 ? new RegExp("[" + confusableChars.join("") + "]") : null;
972
+ const confusableRegex = confusableChars.length > 0 ? new RegExp(
973
+ "[" + confusableChars.map((c) => c.replace(/[\\\]^-]/g, "\\$&")).join("") + "]"
974
+ ) : null;
975
975
  const nonLatinRegex = /[\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u074F\u0900-\u0DFF\u0E00-\u0EFF\u1000-\u109F\u10A0-\u10FF\u1200-\u137F\u13A0-\u13FF\u1400-\u167F\u16A0-\u16FF\u1780-\u17FF\u2C80-\u2CFF\u2D30-\u2D7F\uA4D0-\uA4FF\uA6A0-\uA6FF\uAB70-\uABBF]/;
976
976
  const latinRegex = /[a-zA-Z]/;
977
977
  return async (value) => {
@@ -1015,7 +1015,7 @@ function createNamespaceGuard(config, adapter) {
1015
1015
  const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
1016
1016
  const cacheEnabled = !!config.cache;
1017
1017
  const cacheTtl = config.cache?.ttl ?? 5e3;
1018
- const cacheMaxSize = 1e3;
1018
+ const cacheMaxSize = config.cache?.maxSize ?? 1e3;
1019
1019
  const cacheMap = /* @__PURE__ */ new Map();
1020
1020
  let cacheHits = 0;
1021
1021
  let cacheMisses = 0;
package/dist/index.mjs CHANGED
@@ -115,14 +115,12 @@ function createScrambleStrategy(_pattern) {
115
115
  const candidates = [];
116
116
  const chars = identifier.split("");
117
117
  for (let i = 0; i < chars.length - 1; i++) {
118
- if (chars[i] !== chars[i + 1]) {
119
- const swapped = [...chars];
120
- [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
121
- const candidate = swapped.join("");
122
- if (candidate !== identifier && !seen.has(candidate)) {
123
- seen.add(candidate);
124
- candidates.push(candidate);
125
- }
118
+ const swapped = [...chars];
119
+ [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
120
+ const candidate = swapped.join("");
121
+ if (candidate !== identifier && !seen.has(candidate)) {
122
+ seen.add(candidate);
123
+ candidates.push(candidate);
126
124
  }
127
125
  }
128
126
  return candidates;
@@ -943,7 +941,9 @@ function createHomoglyphValidator(options) {
943
941
  Object.assign(map, options.additionalMappings);
944
942
  }
945
943
  const confusableChars = Object.keys(map);
946
- const confusableRegex = confusableChars.length > 0 ? new RegExp("[" + confusableChars.join("") + "]") : null;
944
+ const confusableRegex = confusableChars.length > 0 ? new RegExp(
945
+ "[" + confusableChars.map((c) => c.replace(/[\\\]^-]/g, "\\$&")).join("") + "]"
946
+ ) : null;
947
947
  const nonLatinRegex = /[\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u074F\u0900-\u0DFF\u0E00-\u0EFF\u1000-\u109F\u10A0-\u10FF\u1200-\u137F\u13A0-\u13FF\u1400-\u167F\u16A0-\u16FF\u1780-\u17FF\u2C80-\u2CFF\u2D30-\u2D7F\uA4D0-\uA4FF\uA6A0-\uA6FF\uAB70-\uABBF]/;
948
948
  const latinRegex = /[a-zA-Z]/;
949
949
  return async (value) => {
@@ -987,7 +987,7 @@ function createNamespaceGuard(config, adapter) {
987
987
  const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
988
988
  const cacheEnabled = !!config.cache;
989
989
  const cacheTtl = config.cache?.ttl ?? 5e3;
990
- const cacheMaxSize = 1e3;
990
+ const cacheMaxSize = config.cache?.maxSize ?? 1e3;
991
991
  const cacheMap = /* @__PURE__ */ new Map();
992
992
  let cacheHits = 0;
993
993
  let cacheMisses = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "namespace-guard",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Check slug/handle uniqueness across multiple database tables with reserved name protection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",