@aria-framework/kit 0.6.0 → 0.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/asyncHandler.js CHANGED
@@ -13,12 +13,22 @@
13
13
  *
14
14
  * Express 5 handles this natively. Kept anyway: it is a no-op there, and removing it would mean
15
15
  * editing every route the day an app upgrades.
16
+ *
17
+ * IT RETURNS THE PROMISE, and that `return` is load-bearing even though Express ignores it.
18
+ * TEST HARNESSES AWAIT THE HANDLER. Both consuming apps drive real route handlers directly —
19
+ * `await callback(req, res, next)` — to exercise a flow without standing up a server. Drop the
20
+ * return and `await undefined` resolves immediately, so every assertion runs BEFORE the handler
21
+ * has finished and a suite that was proving something starts proving nothing.
22
+ *
23
+ * This was found the hard way: 0.6.0 shipped without it, because the original was a one-line
24
+ * arrow whose implicit return was invisible when it was rewritten as a named function. Eight
25
+ * checks in one app's Entra suite failed, all of them looking like authorisation bugs.
16
26
  */
17
27
 
18
28
  'use strict';
19
29
 
20
30
  module.exports = function asyncHandler(fn) {
21
31
  return function (req, res, next) {
22
- Promise.resolve(fn(req, res, next)).catch(next);
32
+ return Promise.resolve(fn(req, res, next)).catch(next);
23
33
  };
24
34
  };
package/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * fullName(first, last) / splitName(full) — person-name helpers
11
11
  * fileSniff.sniff(path) / fileSniff.matches(path, mime)
12
12
  * trackingId.generate() / trackingId.generateUnique(isTaken, maxAttempts)
13
+ * publicId.generate(prefix) / publicId.isValid(value, prefix) — prefixed permanent ids
13
14
  * asyncHandler(fn) — Express async-rejection wrapper
14
15
  * dbTime.parseDbTimestampAsUtc / dbTime.partsInTimezone — a stored timestamp, in a timezone
15
16
  */
@@ -26,6 +27,7 @@ module.exports = {
26
27
  splitName,
27
28
  fileSniff: require('./fileSniff'),
28
29
  trackingId: require('./trackingId'),
30
+ publicId: require('./publicId'),
29
31
  // Page arithmetic for list views: the clamp, the past-the-end correction and the page-link
30
32
  // window, which are the three things every hand-rolled paginator here got wrong or omitted.
31
33
  paginate: require('./paginate').paginate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aria-framework/kit",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Aria App Framework — kit module. Small dependency-free server utilities: open-redirect guard (safeReturnTo), SQL LIKE escaping, magic-byte upload validation (fileSniff), Crockford base32 tracking IDs, and person-name compose/split helpers.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -15,12 +15,13 @@
15
15
  "isEmail.js",
16
16
  "fileSniff.js",
17
17
  "trackingId.js",
18
+ "publicId.js",
18
19
  "personName.js",
19
20
  "paginate.js",
20
21
  "asyncHandler.js",
21
22
  "dbTime.js"
22
23
  ],
23
24
  "scripts": {
24
- "test": "node test/smoke.js && node test/paginate.js && node test/dbTime.js"
25
+ "test": "node test/smoke.js && node test/paginate.js && node test/dbTime.js && node test/publicId.js"
25
26
  }
26
27
  }
package/publicId.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Public identifiers — prefix_XXXXXXXXXXXXXXXX, sixteen Crockford base32 characters.
3
+ *
4
+ * The THIRD identifier kind, beside the integer primary key and the caller's own external ref.
5
+ * The integer id is the database's key (joins, foreign keys) and never leaves the building — it is
6
+ * enumerable and it counts your rows for anyone who sees it. The external ref is the CALLER'S key,
7
+ * agreed at onboarding, optional and editable. This is OURS and permanent: how an API, a URL or a
8
+ * log names an entity, assigned once at creation and never changed. A `cnt_…` in a log line says
9
+ * what it is before anyone looks it up.
10
+ *
11
+ * This is the RAW primitive — it takes a prefix string and knows nothing of any app's entities.
12
+ * The registry of which prefix means which entity (customer, invoice, …) is per-app policy and
13
+ * lives in the app, exactly as `trackingId` leaves the storage existence-check to its caller. The
14
+ * two things every consumer shares — the alphabet and the length — are single-sourced HERE, so no
15
+ * two apps can drift on what a public id physically is.
16
+ *
17
+ * Crockford base32 (no I, L, O, U — same alphabet as trackingId) so ids survive being read aloud
18
+ * and retyped. crypto.randomInt gives an unbiased draw. Sixteen characters is 32^16 ≈ 1.2 × 10^24
19
+ * of space: at that size a check-the-database-first loop buys nothing, so generation is a plain
20
+ * draw and a UNIQUE index on the storing column is the collision backstop — if the astronomically
21
+ * unlikely happens, the INSERT fails loudly instead of two rows sharing a name.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const crypto = require('crypto');
27
+
28
+ const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford base32 (no I L O U)
29
+ const DEFAULT_LENGTH = 16;
30
+
31
+ /** A prefix is a short token that names the KIND — it must not contain the `_` separator. */
32
+ function assertPrefix(prefix) {
33
+ if (typeof prefix !== 'string' || prefix.length === 0 || prefix.includes('_')) {
34
+ throw new Error('publicId: prefix must be a non-empty string with no "_" separator');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A well-formed public id for the given prefix, e.g. generate('cnt') → 'cnt_8HW3PB6XK2M94TQZ'.
40
+ * @param {string} prefix - the KIND token (no underscore)
41
+ * @param {{length?: number}} [opts]
42
+ * @returns {string}
43
+ */
44
+ function generate(prefix, { length = DEFAULT_LENGTH } = {}) {
45
+ assertPrefix(prefix);
46
+ let body = '';
47
+ for (let i = 0; i < length; i++) body += ALPHABET[crypto.randomInt(ALPHABET.length)];
48
+ return `${prefix}_${body}`;
49
+ }
50
+
51
+ /**
52
+ * True when `value` is a well-formed public id for exactly this prefix.
53
+ *
54
+ * The prefix is REQUIRED: this primitive validates against one known prefix, because it has no
55
+ * registry to tell it which prefixes are legitimate. An app that wants "any of my entities" ORs
56
+ * this over its own prefix set (or precompiles a single regex from the exported ALPHABET).
57
+ *
58
+ * @param {string} value
59
+ * @param {string} prefix
60
+ * @param {{length?: number}} [opts]
61
+ * @returns {boolean}
62
+ */
63
+ function isValid(value, prefix, { length = DEFAULT_LENGTH } = {}) {
64
+ assertPrefix(prefix);
65
+ const re = new RegExp(`^${prefix}_[${ALPHABET}]{${length}}$`);
66
+ return re.test(String(value == null ? '' : value));
67
+ }
68
+
69
+ module.exports = { generate, isValid, ALPHABET, DEFAULT_LENGTH };