@astram/guard 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/authentication/argon2id-password-hasher.d.ts +61 -0
  4. package/dist/authentication/argon2id-password-hasher.d.ts.map +1 -0
  5. package/dist/authentication/argon2id-password-hasher.js +108 -0
  6. package/dist/boot/boot-sequence.d.ts +43 -0
  7. package/dist/boot/boot-sequence.d.ts.map +1 -0
  8. package/dist/boot/boot-sequence.js +90 -0
  9. package/dist/boot/boot.module.d.ts +8 -0
  10. package/dist/boot/boot.module.d.ts.map +1 -0
  11. package/dist/boot/boot.module.js +24 -0
  12. package/dist/config/app-config.d.ts +112 -0
  13. package/dist/config/app-config.d.ts.map +1 -0
  14. package/dist/config/app-config.js +94 -0
  15. package/dist/config/config.module.d.ts +12 -0
  16. package/dist/config/config.module.d.ts.map +1 -0
  17. package/dist/config/config.module.js +35 -0
  18. package/dist/enforcement/classification.decorator.d.ts +41 -0
  19. package/dist/enforcement/classification.decorator.d.ts.map +1 -0
  20. package/dist/enforcement/classification.decorator.js +46 -0
  21. package/dist/enforcement/core-seam.d.ts +40 -0
  22. package/dist/enforcement/core-seam.d.ts.map +1 -0
  23. package/dist/enforcement/core-seam.js +38 -0
  24. package/dist/enforcement/enforcement.module.d.ts +18 -0
  25. package/dist/enforcement/enforcement.module.d.ts.map +1 -0
  26. package/dist/enforcement/enforcement.module.js +40 -0
  27. package/dist/enforcement/global-enforcement.guard.d.ts +31 -0
  28. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -0
  29. package/dist/enforcement/global-enforcement.guard.js +76 -0
  30. package/dist/index.d.ts +26 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +88 -0
  33. package/dist/module/shield.module.d.ts +79 -0
  34. package/dist/module/shield.module.d.ts.map +1 -0
  35. package/dist/module/shield.module.js +108 -0
  36. package/dist/session/crypto-adapters.d.ts +60 -0
  37. package/dist/session/crypto-adapters.d.ts.map +1 -0
  38. package/dist/session/crypto-adapters.js +80 -0
  39. package/dist/session/index.d.ts +17 -0
  40. package/dist/session/index.d.ts.map +1 -0
  41. package/dist/session/index.js +32 -0
  42. package/dist/session/mariadb-session-record-store.d.ts +50 -0
  43. package/dist/session/mariadb-session-record-store.d.ts.map +1 -0
  44. package/dist/session/mariadb-session-record-store.js +162 -0
  45. package/dist/session/redis-hot-session-store.d.ts +30 -0
  46. package/dist/session/redis-hot-session-store.d.ts.map +1 -0
  47. package/dist/session/redis-hot-session-store.js +102 -0
  48. package/package.json +52 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * `RedisHotSessionStore` — the concrete hot {@link HotSessionStore} for
4
+ * `@astram/guard`, backed by `ioredis` against the Layer-2 Redis (ADR-0013 hot
5
+ * tier). Serves the per-request read path and is (re)populated at lifecycle events.
6
+ *
7
+ * ADR-0003 (why HERE, not Core): `ioredis` is a real runtime dependency; Core
8
+ * defines only the PORT. Injected into `createSessionAuthority({ hotStore })` and
9
+ * used by the free `readHotSession(hotStore, id)` per-request path.
10
+ *
11
+ * ⛔ THE CRITICAL CONTRACT (ADR-0013/0016): `getHotSession` MUST distinguish
12
+ * - `{ kind: 'found', state }` — a live hot record,
13
+ * - `{ kind: 'absent' }` — Redis is UP and simply has no such key (a miss),
14
+ * - `{ kind: 'unavailable' }` — Redis CANNOT answer (down/timeout/refused).
15
+ * The engine fails CLOSED on `unavailable` and NEVER falls back to the durable
16
+ * tier. Conflating unavailable→absent would fail open on a blip; conflating
17
+ * absent→unavailable would log users out on a normal miss. So we catch connection
18
+ * errors and map ONLY those to `unavailable`; a clean `null` GET reply is `absent`.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.RedisHotSessionStore = exports.SHIELD_HOT_SESSION_STORE = void 0;
22
+ /** The DI token the store is bound under. */
23
+ exports.SHIELD_HOT_SESSION_STORE = Symbol('SHIELD_HOT_SESSION_STORE');
24
+ /**
25
+ * A connection-level failure — Redis genuinely cannot answer. ioredis surfaces
26
+ * these as errors whose `code` is a socket/connection code, or (when the client
27
+ * is offline and not queuing) a MaxRetriesPerRequest/"Connection is closed"
28
+ * message. We treat any GET-time error as "cannot answer" → `unavailable`
29
+ * (fail-closed) rather than risk mis-reading it as a normal miss.
30
+ */
31
+ function isConnectionFailure(err) {
32
+ if (err === null || typeof err !== 'object')
33
+ return false;
34
+ const e = err;
35
+ const code = typeof e.code === 'string' ? e.code : '';
36
+ const message = typeof e.message === 'string' ? e.message : '';
37
+ const CONN_CODES = new Set([
38
+ 'ECONNREFUSED',
39
+ 'ECONNRESET',
40
+ 'ETIMEDOUT',
41
+ 'EPIPE',
42
+ 'ENOTFOUND',
43
+ 'EAI_AGAIN',
44
+ 'NR_CLOSED',
45
+ ]);
46
+ if (CONN_CODES.has(code))
47
+ return true;
48
+ return (/connection is closed/i.test(message) ||
49
+ /max retries per request/i.test(message) ||
50
+ /connection to redis/i.test(message) ||
51
+ /stream isn't writeable/i.test(message));
52
+ }
53
+ class RedisHotSessionStore {
54
+ redis;
55
+ constructor(redis) {
56
+ this.redis = redis;
57
+ }
58
+ async putHotSession(key, state, ttlMs) {
59
+ // JSON blob under the key with a PX (ms) TTL = the access-token TTL. cachedFacts
60
+ // is an opaque slot — we serialize whatever the engine passes (null at seed).
61
+ const payload = JSON.stringify(state);
62
+ await this.redis.set(key, payload, 'PX', Math.max(1, Math.floor(ttlMs)));
63
+ }
64
+ async getHotSession(key) {
65
+ let raw;
66
+ try {
67
+ raw = await this.redis.get(key);
68
+ }
69
+ catch (err) {
70
+ // ⛔ Cannot answer → fail-closed signal. NEVER a silent miss.
71
+ if (isConnectionFailure(err))
72
+ return { kind: 'unavailable' };
73
+ // Any other error is also treated as cannot-answer (conservative: never
74
+ // downgrade an unknown fault to a normal `absent` that could fail open).
75
+ return { kind: 'unavailable' };
76
+ }
77
+ if (raw === null)
78
+ return { kind: 'absent' }; // Redis is UP; genuine miss.
79
+ try {
80
+ const state = JSON.parse(raw);
81
+ if (state === null || typeof state !== 'object' || typeof state.sessionId !== 'string') {
82
+ // Corrupt payload — treat as a miss (the value will be re-populated at the
83
+ // next refresh); do NOT surface `found` with a malformed state.
84
+ return { kind: 'absent' };
85
+ }
86
+ return { kind: 'found', state };
87
+ }
88
+ catch {
89
+ return { kind: 'absent' };
90
+ }
91
+ }
92
+ async deleteHotSession(key) {
93
+ try {
94
+ await this.redis.del(key);
95
+ }
96
+ catch {
97
+ // Best-effort delete; a delete against a down Redis is retried at the next
98
+ // lifecycle event, and the durable revoke already refuses future refreshes.
99
+ }
100
+ }
101
+ }
102
+ exports.RedisHotSessionStore = RedisHotSessionStore;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@astram/guard",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
6
+ "type": "commonjs",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json",
24
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
25
+ "test": "vitest run"
26
+ },
27
+ "dependencies": {
28
+ "@astram/core": "workspace:*",
29
+ "argon2": "^0.41.1",
30
+ "ioredis": "^5.4.1",
31
+ "mariadb": "^3.4.0"
32
+ },
33
+ "peerDependencies": {
34
+ "@nestjs/common": "^11",
35
+ "@nestjs/core": "^11",
36
+ "reflect-metadata": "^0.2",
37
+ "rxjs": "^7"
38
+ },
39
+ "devDependencies": {
40
+ "@nestjs/common": "^11.0.0",
41
+ "@nestjs/core": "^11.0.0",
42
+ "@nestjs/testing": "^11.0.0",
43
+ "@types/node": "^22.0.0",
44
+ "@types/supertest": "^6.0.2",
45
+ "reflect-metadata": "^0.2.2",
46
+ "rxjs": "^7.8.1",
47
+ "supertest": "^7.0.0",
48
+ "typescript": "^5.7.0",
49
+ "unplugin-swc": "^1.5.1",
50
+ "vitest": "^2.1.8"
51
+ }
52
+ }