@c9up/warden 0.1.20 → 0.1.22
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/dist/AuthManager.d.ts +0 -3
- package/dist/AuthManager.d.ts.map +1 -1
- package/dist/AuthManager.js +21 -21
- package/dist/AuthManager.js.map +1 -1
- package/dist/AuthRateLimiter.d.ts +1 -6
- package/dist/AuthRateLimiter.d.ts.map +1 -1
- package/dist/AuthRateLimiter.js +31 -31
- package/dist/AuthRateLimiter.js.map +1 -1
- package/dist/RefreshTokenStore.d.ts +1 -1
- package/dist/RefreshTokenStore.d.ts.map +1 -1
- package/dist/RefreshTokenStore.js +9 -9
- package/dist/RefreshTokenStore.js.map +1 -1
- package/dist/TokenBlacklist.d.ts +1 -1
- package/dist/TokenBlacklist.d.ts.map +1 -1
- package/dist/TokenBlacklist.js +6 -6
- package/dist/TokenBlacklist.js.map +1 -1
- package/dist/augmentations.d.ts +0 -1
- package/dist/augmentations.d.ts.map +1 -1
- package/dist/bouncer/AuthorizationResponse.d.ts +6 -0
- package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -1
- package/dist/bouncer/AuthorizationResponse.js +6 -0
- package/dist/bouncer/AuthorizationResponse.js.map +1 -1
- package/dist/native/generated.d.ts +8 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/native.d.ts +8 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/rights/MemoryRightsStore.d.ts +1 -6
- package/dist/rights/MemoryRightsStore.d.ts.map +1 -1
- package/dist/rights/MemoryRightsStore.js +10 -10
- package/dist/rights/MemoryRightsStore.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +3 -2
- package/scripts/build-napi-types.mjs +68 -0
- package/scripts/generate-napi-types.mjs +153 -0
- package/src/AuthManager.ts +21 -21
- package/src/AuthRateLimiter.ts +29 -29
- package/src/RefreshTokenStore.ts +9 -9
- package/src/TokenBlacklist.ts +6 -6
- package/src/augmentations.ts +0 -2
- package/src/bouncer/AuthorizationResponse.ts +6 -0
- package/src/native/generated.ts +23 -0
- package/src/native.ts +8 -9
- package/src/rights/MemoryRightsStore.ts +10 -10
|
@@ -12,12 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type RightsStore, type Scope } from "./types.js";
|
|
14
14
|
export declare class MemoryRightsStore implements RightsStore {
|
|
15
|
-
|
|
16
|
-
private readonly roleDefs;
|
|
17
|
-
/** scopeKey → userId → roles */
|
|
18
|
-
private readonly userRoleMap;
|
|
19
|
-
/** scopeKey → userId → directly granted permissions */
|
|
20
|
-
private readonly userGrantMap;
|
|
15
|
+
#private;
|
|
21
16
|
rolePermissions(role: string, scope: Scope): Promise<readonly string[]>;
|
|
22
17
|
userRoles(userId: string, scope: Scope): Promise<readonly string[]>;
|
|
23
18
|
userGrants(userId: string, scope: Scope): Promise<readonly string[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryRightsStore.d.ts","sourceRoot":"","sources":["../../src/rights/MemoryRightsStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAY,MAAM,YAAY,CAAC;AAEpE,qBAAa,iBAAkB,YAAW,WAAW
|
|
1
|
+
{"version":3,"file":"MemoryRightsStore.d.ts","sourceRoot":"","sources":["../../src/rights/MemoryRightsStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAY,MAAM,YAAY,CAAC;AAEpE,qBAAa,iBAAkB,YAAW,WAAW;;IAU9C,eAAe,CACpB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,KAAK,GACV,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAIvB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAInE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAM1E,UAAU,CACT,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,KAAK,GAAE,KAAgB,GACrB,IAAI;IAOP,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,KAAgB,GAAG,IAAI;IAKvE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,GAAE,KAAgB,GAAG,IAAI;IAKxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,GAAE,KAAgB,GAAG,IAAI;CAIzE"}
|
|
@@ -13,39 +13,39 @@
|
|
|
13
13
|
import { scopeKey } from "./types.js";
|
|
14
14
|
export class MemoryRightsStore {
|
|
15
15
|
/** scopeKey → role → permissions */
|
|
16
|
-
roleDefs = new Map();
|
|
16
|
+
#roleDefs = new Map();
|
|
17
17
|
/** scopeKey → userId → roles */
|
|
18
|
-
userRoleMap = new Map();
|
|
18
|
+
#userRoleMap = new Map();
|
|
19
19
|
/** scopeKey → userId → directly granted permissions */
|
|
20
|
-
userGrantMap = new Map();
|
|
20
|
+
#userGrantMap = new Map();
|
|
21
21
|
// — read contract —
|
|
22
22
|
async rolePermissions(role, scope) {
|
|
23
|
-
return snapshot(this
|
|
23
|
+
return snapshot(this.#roleDefs.get(scopeKey(scope))?.get(role));
|
|
24
24
|
}
|
|
25
25
|
async userRoles(userId, scope) {
|
|
26
|
-
return snapshot(this
|
|
26
|
+
return snapshot(this.#userRoleMap.get(scopeKey(scope))?.get(userId));
|
|
27
27
|
}
|
|
28
28
|
async userGrants(userId, scope) {
|
|
29
|
-
return snapshot(this
|
|
29
|
+
return snapshot(this.#userGrantMap.get(scopeKey(scope))?.get(userId));
|
|
30
30
|
}
|
|
31
31
|
// — seeding (in-memory driver only; not part of RightsStore) —
|
|
32
32
|
defineRole(role, permissions, scope = "global") {
|
|
33
|
-
const set = bucket(this
|
|
33
|
+
const set = bucket(this.#roleDefs, scopeKey(scope), role);
|
|
34
34
|
set.clear();
|
|
35
35
|
for (const perm of permissions)
|
|
36
36
|
set.add(perm);
|
|
37
37
|
return this;
|
|
38
38
|
}
|
|
39
39
|
assignRole(userId, role, scope = "global") {
|
|
40
|
-
bucket(this
|
|
40
|
+
bucket(this.#userRoleMap, scopeKey(scope), userId).add(role);
|
|
41
41
|
return this;
|
|
42
42
|
}
|
|
43
43
|
grant(userId, permission, scope = "global") {
|
|
44
|
-
bucket(this
|
|
44
|
+
bucket(this.#userGrantMap, scopeKey(scope), userId).add(permission);
|
|
45
45
|
return this;
|
|
46
46
|
}
|
|
47
47
|
revoke(userId, permission, scope = "global") {
|
|
48
|
-
this
|
|
48
|
+
this.#userGrantMap.get(scopeKey(scope))?.get(userId)?.delete(permission);
|
|
49
49
|
return this;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryRightsStore.js","sourceRoot":"","sources":["../../src/rights/MemoryRightsStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgC,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,OAAO,iBAAiB;IAC7B,oCAAoC;
|
|
1
|
+
{"version":3,"file":"MemoryRightsStore.js","sourceRoot":"","sources":["../../src/rights/MemoryRightsStore.ts"],"names":[],"mappings":"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"}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/warden",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Warden — Authentication & authorization for the Ream framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
"scripts": {
|
|
83
83
|
"build": "tsc -p tsconfig.build.json",
|
|
84
84
|
"build:rust": "cargo build --release -p warden-engine-napi",
|
|
85
|
-
"build:napi": "pnpm build:rust && node scripts/copy-napi.mjs",
|
|
85
|
+
"build:napi": "pnpm build:rust && pnpm build:napi-types && node scripts/copy-napi.mjs",
|
|
86
|
+
"build:napi-types": "node scripts/build-napi-types.mjs",
|
|
86
87
|
"test": "vitest run",
|
|
87
88
|
"test:rust": "cargo test -p warden-engine",
|
|
88
89
|
"lint": "biome check src/",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerate `src/native/generated.ts` from the Rust.
|
|
4
|
+
*
|
|
5
|
+
* Node rather than a shell script because this runs inside `build:napi`, and
|
|
6
|
+
* that runs on the Windows prebuild runner too: a bash `mktemp` there hands
|
|
7
|
+
* cargo a `/tmp/...` path the native proc-macro cannot write to, so the
|
|
8
|
+
* type-def file comes back empty and the build fails for no visible reason.
|
|
9
|
+
*
|
|
10
|
+
* One crate at a time on purpose: napi-derive APPENDS to `TYPE_DEF_TMP_PATH`
|
|
11
|
+
* while cargo compiles, and a parallel build interleaves the writes —
|
|
12
|
+
* definitions go missing, silently, and the generated file comes out short.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const CRATES = ['warden-engine-napi']
|
|
23
|
+
|
|
24
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
25
|
+
const output = join('src', 'native', 'generated.ts')
|
|
26
|
+
const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const lines = []
|
|
30
|
+
for (const crate of CRATES) {
|
|
31
|
+
const perCrate = join(scratch, `${crate}.jsonl`)
|
|
32
|
+
writeFileSync(perCrate, '')
|
|
33
|
+
execFileSync('cargo', ['build', '-p', crate], {
|
|
34
|
+
cwd: packageRoot,
|
|
35
|
+
env: { ...process.env, TYPE_DEF_TMP_PATH: perCrate },
|
|
36
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
37
|
+
})
|
|
38
|
+
const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
|
|
39
|
+
if (emitted.length === 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[napi-types] ${crate} emitted nothing — is napi-derive's "type-def" feature on?`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
lines.push(...emitted)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const combined = join(scratch, 'combined.jsonl')
|
|
48
|
+
writeFileSync(combined, `${lines.join('\n')}\n`)
|
|
49
|
+
|
|
50
|
+
execFileSync(
|
|
51
|
+
process.execPath,
|
|
52
|
+
[join('scripts', 'generate-napi-types.mjs'), combined, output],
|
|
53
|
+
{ cwd: packageRoot, stdio: 'inherit' },
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Formatted here rather than excluded from the linter: the file is checked
|
|
57
|
+
// in, so it should read like the rest of the tree — and formatting it at
|
|
58
|
+
// generation means it never shows up as a diff someone has to fix by hand.
|
|
59
|
+
// Resolved through the package entry rather than `node_modules/.bin`, whose
|
|
60
|
+
// shim is a shell script the Windows runner cannot execute directly.
|
|
61
|
+
const biome = createRequire(import.meta.url).resolve('@biomejs/biome/bin/biome')
|
|
62
|
+
execFileSync(process.execPath, [biome, 'format', '--write', output], {
|
|
63
|
+
cwd: packageRoot,
|
|
64
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
65
|
+
})
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
68
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the TypeScript surface of the native module FROM the Rust.
|
|
4
|
+
*
|
|
5
|
+
* `napi-derive`'s `type-def` feature emits one JSON line per exported item
|
|
6
|
+
* while cargo compiles. Those lines are the source of truth: they are derived
|
|
7
|
+
* from the `#[napi]` items themselves, so a signature cannot drift from the
|
|
8
|
+
* Rust without this file changing.
|
|
9
|
+
*
|
|
10
|
+
* Hand-written interfaces are what this replaces, and they do drift — nothing
|
|
11
|
+
* on the TypeScript side notices when a `pub fn` gains a parameter or stops
|
|
12
|
+
* being `async`.
|
|
13
|
+
*
|
|
14
|
+
* One thing napi-rs cannot infer: the shape of a `JsFunction` callback. It
|
|
15
|
+
* emits `(...args: any[]) => any`. Any such parameter is refined below, by
|
|
16
|
+
* name, and the script fails if a refinement no longer matches anything — so
|
|
17
|
+
* the table cannot rot either.
|
|
18
|
+
*
|
|
19
|
+
* TYPE_DEF_TMP_PATH=<file> cargo build -p <crate>
|
|
20
|
+
* node scripts/generate-napi-types.mjs <file> <out.d.ts>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback signatures napi-rs erases to `any`, restored here.
|
|
27
|
+
*
|
|
28
|
+
* Keyed by `<owner>.<method>`; the value replaces the whole parameter. Every
|
|
29
|
+
* entry must match, or the script fails: a stale refinement is how a hand
|
|
30
|
+
* annotation quietly stops describing the Rust.
|
|
31
|
+
*
|
|
32
|
+
* Empty while this crate exposes no `JsFunction` parameter — add an entry the
|
|
33
|
+
* day one appears, rather than letting an `any` through.
|
|
34
|
+
*/
|
|
35
|
+
const CALLBACK_REFINEMENTS = {}
|
|
36
|
+
|
|
37
|
+
const [input, output] = process.argv.slice(2)
|
|
38
|
+
if (!input || !output) {
|
|
39
|
+
console.error('usage: generate-napi-types.mjs <type-def file> <out.d.ts>')
|
|
40
|
+
process.exit(2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entries = readFileSync(input, 'utf8')
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((l) => l.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((l) => JSON.parse(l))
|
|
48
|
+
|
|
49
|
+
const used = new Set()
|
|
50
|
+
|
|
51
|
+
/** Apply the refinements that belong to `owner`, tracking which ones matched. */
|
|
52
|
+
function refine(owner, body) {
|
|
53
|
+
let out = body
|
|
54
|
+
for (const [key, { from, to }] of Object.entries(CALLBACK_REFINEMENTS)) {
|
|
55
|
+
const [entryOwner] = key.split('.')
|
|
56
|
+
if (entryOwner !== owner) continue
|
|
57
|
+
if (out.includes(from)) {
|
|
58
|
+
out = out.replace(from, to)
|
|
59
|
+
used.add(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JSDoc as emitted by napi-derive, indented to sit above its member.
|
|
67
|
+
*
|
|
68
|
+
* `*` followed by `/` inside the text closes the comment early — a Rust doc
|
|
69
|
+
* example holding a cron expression (`0 */5 * * *`) is enough to do it, and
|
|
70
|
+
* the generated file then fails to parse. Escaped rather than stripped, so the
|
|
71
|
+
* example still reads correctly.
|
|
72
|
+
*/
|
|
73
|
+
function docBlock(doc, indent = '') {
|
|
74
|
+
if (!doc) return ''
|
|
75
|
+
// Escape every `*/` EXCEPT the one that closes the block. A Rust doc example
|
|
76
|
+
// holding a cron expression (`0 */5 * * *`) closes the comment early
|
|
77
|
+
// otherwise, and the generated file stops parsing — but escaping the closer
|
|
78
|
+
// too breaks it just as thoroughly, whether the block spans one line or many.
|
|
79
|
+
const closer = doc.lastIndexOf('*/')
|
|
80
|
+
const escaped =
|
|
81
|
+
closer === -1
|
|
82
|
+
? doc
|
|
83
|
+
: doc.slice(0, closer).replaceAll('*/', '*\\/') + doc.slice(closer)
|
|
84
|
+
return (
|
|
85
|
+
escaped
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((l) => l.length > 0)
|
|
88
|
+
.map((l) => `${indent}${l}`)
|
|
89
|
+
.join('\n') + '\n'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const interfaces = entries.filter((e) => e.kind === 'interface')
|
|
94
|
+
const structs = entries.filter((e) => e.kind === 'struct')
|
|
95
|
+
const impls = new Map(entries.filter((e) => e.kind === 'impl').map((e) => [e.name, e]))
|
|
96
|
+
const fns = entries.filter((e) => e.kind === 'fn')
|
|
97
|
+
|
|
98
|
+
const out = [
|
|
99
|
+
'// GENERATED FROM THE RUST — do not edit.',
|
|
100
|
+
'//',
|
|
101
|
+
"// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def",
|
|
102
|
+
'// output. Editing this file by hand puts it back where it started: a',
|
|
103
|
+
'// description that can disagree with the code it describes.',
|
|
104
|
+
'',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for (const iface of interfaces) {
|
|
108
|
+
out.push(docBlock(iface.js_doc))
|
|
109
|
+
out.push(`export interface ${iface.name} {`)
|
|
110
|
+
for (const line of iface.def.split('\n')) {
|
|
111
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
112
|
+
}
|
|
113
|
+
out.push('}', '')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const struct of structs) {
|
|
117
|
+
const impl = impls.get(struct.name)
|
|
118
|
+
out.push(docBlock(struct.js_doc))
|
|
119
|
+
out.push(`export declare class ${struct.name} {`)
|
|
120
|
+
if (impl) {
|
|
121
|
+
for (const line of refine(struct.name, impl.def).split('\n')) {
|
|
122
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
out.push('}', '')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const fn of fns) {
|
|
129
|
+
out.push(docBlock(fn.js_doc))
|
|
130
|
+
// napi-derive emits the whole declaration for a function, unlike a struct
|
|
131
|
+
// where `def` holds only the members.
|
|
132
|
+
const declaration = fn.def.trim()
|
|
133
|
+
out.push(
|
|
134
|
+
declaration.startsWith('export declare function')
|
|
135
|
+
? `${declaration};`
|
|
136
|
+
: `export declare function ${fn.name}${declaration};`,
|
|
137
|
+
'',
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
|
|
142
|
+
if (stale.length > 0) {
|
|
143
|
+
console.error(
|
|
144
|
+
`[napi-types] refinement(s) that matched nothing: ${stale.join(', ')}\n` +
|
|
145
|
+
'[napi-types] the Rust changed under them — update or remove the entry.',
|
|
146
|
+
)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
writeFileSync(output, out.join('\n'))
|
|
151
|
+
console.log(
|
|
152
|
+
`[napi-types] ${output} — ${interfaces.length} interface(s), ${structs.length} class(es), ${fns.length} function(s)`,
|
|
153
|
+
)
|
package/src/AuthManager.ts
CHANGED
|
@@ -146,9 +146,9 @@ export interface WardenEmitter {
|
|
|
146
146
|
export class AuthManager {
|
|
147
147
|
// AdonisJS names: `guards` (name → strategy) and `default` (the guard used
|
|
148
148
|
// when none is named). Renamed from the previous `strategies`/`defaultStrategy`.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
#guards: Map<string, AuthStrategy> = new Map();
|
|
150
|
+
#default: string;
|
|
151
|
+
readonly #rights: RightsResolver;
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* Where auth events go. Structural and optional: warden is a leaf and must
|
|
@@ -195,10 +195,10 @@ export class AuthManager {
|
|
|
195
195
|
"AuthManager: config must supply `default` + `guards` (or the legacy `defaultStrategy` + `strategies`).",
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
|
-
this
|
|
199
|
-
this
|
|
198
|
+
this.#default = defaultGuard;
|
|
199
|
+
this.#rights = config.rights ?? new RightsResolver(new MemoryRightsStore());
|
|
200
200
|
for (const [name, strategy] of Object.entries(guards)) {
|
|
201
|
-
this
|
|
201
|
+
this.#guards.set(name, strategy);
|
|
202
202
|
}
|
|
203
203
|
// Fail-fast at construction: an AuthManager with zero guards is a
|
|
204
204
|
// configuration bug. Previously, an empty `guards: {}` passed the
|
|
@@ -206,13 +206,13 @@ export class AuthManager {
|
|
|
206
206
|
// runtime when `getStrategy('jwt')` threw — opaque 401 or 500 instead
|
|
207
207
|
// of a boot-time INVALID_CONFIG that points the operator at the missing
|
|
208
208
|
// `config.auth.jwt` (or other guard).
|
|
209
|
-
if (this
|
|
209
|
+
if (this.#guards.size === 0) {
|
|
210
210
|
throw new WardenError(
|
|
211
211
|
"INVALID_CONFIG",
|
|
212
212
|
`AuthManager: no authentication guards registered. Configure at least one guard (e.g. config.warden.auth.jwt) before booting WardenProvider.`,
|
|
213
213
|
);
|
|
214
214
|
}
|
|
215
|
-
if (!this
|
|
215
|
+
if (!this.#guards.has(defaultGuard)) {
|
|
216
216
|
throw new WardenError(
|
|
217
217
|
"INVALID_CONFIG",
|
|
218
218
|
`default guard '${defaultGuard}' is not present in guards`,
|
|
@@ -285,7 +285,7 @@ export class AuthManager {
|
|
|
285
285
|
if (!isTokenIssuer(strategy)) {
|
|
286
286
|
throw new WardenError(
|
|
287
287
|
"STRATEGY_CANNOT_ISSUE",
|
|
288
|
-
`Auth strategy '${strategyName ?? this
|
|
288
|
+
`Auth strategy '${strategyName ?? this.#default}' cannot issue tokens.`,
|
|
289
289
|
{
|
|
290
290
|
hint: "issueFor() needs a token-minting strategy (e.g. JwtStrategy with signToken). Session / API-key strategies don't mint tokens.",
|
|
291
291
|
},
|
|
@@ -306,7 +306,7 @@ export class AuthManager {
|
|
|
306
306
|
user: UserPayload,
|
|
307
307
|
scope: Scope = "global",
|
|
308
308
|
): Promise<EffectivePermissions> {
|
|
309
|
-
return this
|
|
309
|
+
return this.#rights.resolve(user, scope);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/**
|
|
@@ -381,7 +381,7 @@ export class AuthManager {
|
|
|
381
381
|
if (!seam) {
|
|
382
382
|
throw new WardenError(
|
|
383
383
|
"STRATEGY_HAS_NO_CLIENT",
|
|
384
|
-
`Auth strategy '${guard ?? this
|
|
384
|
+
`Auth strategy '${guard ?? this.#default}' has no authenticateAsClient() — it cannot forge a client request.`,
|
|
385
385
|
{
|
|
386
386
|
hint: "Implement authenticateAsClient() on the strategy, or authenticate through a real login in the test.",
|
|
387
387
|
},
|
|
@@ -406,8 +406,8 @@ export class AuthManager {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
getStrategy(name?: string): AuthStrategy {
|
|
409
|
-
const strategyName = name ?? this
|
|
410
|
-
const strategy = this
|
|
409
|
+
const strategyName = name ?? this.#default;
|
|
410
|
+
const strategy = this.#guards.get(strategyName);
|
|
411
411
|
if (!strategy) {
|
|
412
412
|
throw new WardenError(
|
|
413
413
|
"STRATEGY_NOT_FOUND",
|
|
@@ -426,17 +426,17 @@ export class AuthManager {
|
|
|
426
426
|
* declares none.
|
|
427
427
|
*/
|
|
428
428
|
get defaultStrategyName(): string {
|
|
429
|
-
return this
|
|
429
|
+
return this.#default;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
/** Register a new strategy at runtime. */
|
|
433
433
|
registerStrategy(name: string, strategy: AuthStrategy): void {
|
|
434
|
-
this
|
|
434
|
+
this.#guards.set(name, strategy);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
/** Get all registered strategy names. */
|
|
438
438
|
getStrategyNames(): string[] {
|
|
439
|
-
return [...this
|
|
439
|
+
return [...this.#guards.keys()];
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
/**
|
|
@@ -458,7 +458,7 @@ export class AuthManager {
|
|
|
458
458
|
if (!isLoginCapable(strategy)) {
|
|
459
459
|
throw new WardenError(
|
|
460
460
|
"STRATEGY_CANNOT_LOGIN",
|
|
461
|
-
`Auth strategy '${strategyName ?? this
|
|
461
|
+
`Auth strategy '${strategyName ?? this.#default}' does not support session login.`,
|
|
462
462
|
{
|
|
463
463
|
hint: "login()/logout() need a stateful guard (e.g. SessionStrategy). JWT / API-key guards are stateless — mint a token with issueFor() instead.",
|
|
464
464
|
},
|
|
@@ -466,12 +466,12 @@ export class AuthManager {
|
|
|
466
466
|
}
|
|
467
467
|
const prefix = authEventPrefix(strategy.name);
|
|
468
468
|
this.#emit(`${prefix}:login_attempted`, {
|
|
469
|
-
guardName: strategyName ?? this
|
|
469
|
+
guardName: strategyName ?? this.#default,
|
|
470
470
|
user,
|
|
471
471
|
});
|
|
472
472
|
await strategy.login(user, session);
|
|
473
473
|
this.#emit(`${prefix}:login_succeeded`, {
|
|
474
|
-
guardName: strategyName ?? this
|
|
474
|
+
guardName: strategyName ?? this.#default,
|
|
475
475
|
user,
|
|
476
476
|
sessionId: undefined,
|
|
477
477
|
});
|
|
@@ -487,12 +487,12 @@ export class AuthManager {
|
|
|
487
487
|
if (!isLoginCapable(strategy)) {
|
|
488
488
|
throw new WardenError(
|
|
489
489
|
"STRATEGY_CANNOT_LOGIN",
|
|
490
|
-
`Auth strategy '${strategyName ?? this
|
|
490
|
+
`Auth strategy '${strategyName ?? this.#default}' does not support session logout.`,
|
|
491
491
|
);
|
|
492
492
|
}
|
|
493
493
|
await strategy.logout(session);
|
|
494
494
|
this.#emit(`${authEventPrefix(strategy.name)}:logged_out`, {
|
|
495
|
-
guardName: strategyName ?? this
|
|
495
|
+
guardName: strategyName ?? this.#default,
|
|
496
496
|
user: null,
|
|
497
497
|
error: null,
|
|
498
498
|
});
|
package/src/AuthRateLimiter.ts
CHANGED
|
@@ -17,16 +17,16 @@ interface AttemptEntry {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export class AuthRateLimiter {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
#maxAttempts: number;
|
|
21
|
+
#window: number;
|
|
22
|
+
#store: Map<string, AttemptEntry> = new Map();
|
|
23
|
+
#maxStoreSize = 100_000;
|
|
24
24
|
|
|
25
25
|
constructor(config?: AuthRateLimiterConfig) {
|
|
26
|
-
this
|
|
27
|
-
if (this
|
|
28
|
-
this
|
|
29
|
-
if (this
|
|
26
|
+
this.#maxAttempts = config?.maxAttempts ?? 5;
|
|
27
|
+
if (this.#maxAttempts <= 0) this.#maxAttempts = 1;
|
|
28
|
+
this.#window = (config?.windowSeconds ?? 900) * 1000;
|
|
29
|
+
if (this.#window <= 0) this.#window = 900_000; // prevent bypass via 0/negative
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Check if an attempt is allowed. Returns false if rate limited. */
|
|
@@ -38,7 +38,7 @@ export class AuthRateLimiter {
|
|
|
38
38
|
const ipKey = `ip:${ipNorm}`;
|
|
39
39
|
const idKey = `id:${identifierNorm}`;
|
|
40
40
|
|
|
41
|
-
return this
|
|
41
|
+
return this.#checkKey(ipKey, now) && this.#checkKey(idKey, now);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Record a failed attempt. */
|
|
@@ -46,51 +46,51 @@ export class AuthRateLimiter {
|
|
|
46
46
|
const now = Date.now();
|
|
47
47
|
const ipNorm = normalizeIp(ip);
|
|
48
48
|
const identifierNorm = normalizeIdentifier(identifier);
|
|
49
|
-
this
|
|
50
|
-
this
|
|
49
|
+
this.#increment(`ip:${ipNorm}`, now);
|
|
50
|
+
this.#increment(`id:${identifierNorm}`, now);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Reset counters on successful login. */
|
|
54
54
|
recordSuccess(ip: string, identifier: string): void {
|
|
55
55
|
const ipNorm = normalizeIp(ip);
|
|
56
56
|
const identifierNorm = normalizeIdentifier(identifier);
|
|
57
|
-
this
|
|
58
|
-
this
|
|
57
|
+
this.#store.delete(`ip:${ipNorm}`);
|
|
58
|
+
this.#store.delete(`id:${identifierNorm}`);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Get remaining attempts for an identifier. */
|
|
62
62
|
remaining(ip: string, identifier: string): number {
|
|
63
63
|
const ipNorm = normalizeIp(ip);
|
|
64
64
|
const identifierNorm = normalizeIdentifier(identifier);
|
|
65
|
-
const ipEntry = this
|
|
66
|
-
const idEntry = this
|
|
65
|
+
const ipEntry = this.#store.get(`ip:${ipNorm}`);
|
|
66
|
+
const idEntry = this.#store.get(`id:${identifierNorm}`);
|
|
67
67
|
const ipRemaining = ipEntry
|
|
68
|
-
? Math.max(0, this
|
|
69
|
-
: this
|
|
68
|
+
? Math.max(0, this.#maxAttempts - ipEntry.count)
|
|
69
|
+
: this.#maxAttempts;
|
|
70
70
|
const idRemaining = idEntry
|
|
71
|
-
? Math.max(0, this
|
|
72
|
-
: this
|
|
71
|
+
? Math.max(0, this.#maxAttempts - idEntry.count)
|
|
72
|
+
: this.#maxAttempts;
|
|
73
73
|
return Math.min(ipRemaining, idRemaining);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
const entry = this
|
|
76
|
+
#checkKey(key: string, now: number): boolean {
|
|
77
|
+
const entry = this.#store.get(key);
|
|
78
78
|
if (!entry || entry.resetAt < now) return true;
|
|
79
|
-
return entry.count < this
|
|
79
|
+
return entry.count < this.#maxAttempts;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
#increment(key: string, now: number): void {
|
|
83
83
|
// Evict expired entries when store grows too large (prevent OOM)
|
|
84
|
-
if (this
|
|
85
|
-
for (const [k, v] of this
|
|
86
|
-
if (v.resetAt < now) this
|
|
84
|
+
if (this.#store.size > this.#maxStoreSize) {
|
|
85
|
+
for (const [k, v] of this.#store) {
|
|
86
|
+
if (v.resetAt < now) this.#store.delete(k);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
let entry = this
|
|
90
|
+
let entry = this.#store.get(key);
|
|
91
91
|
if (!entry || entry.resetAt < now) {
|
|
92
|
-
entry = { count: 0, resetAt: now + this
|
|
93
|
-
this
|
|
92
|
+
entry = { count: 0, resetAt: now + this.#window };
|
|
93
|
+
this.#store.set(key, entry);
|
|
94
94
|
}
|
|
95
95
|
entry.count++;
|
|
96
96
|
}
|
package/src/RefreshTokenStore.ts
CHANGED
|
@@ -27,36 +27,36 @@ export interface RefreshTokenDriver {
|
|
|
27
27
|
|
|
28
28
|
/** In-memory driver for development. */
|
|
29
29
|
export class MemoryRefreshTokenDriver implements RefreshTokenDriver {
|
|
30
|
-
|
|
30
|
+
#tokens: Map<string, StoredRefreshToken> = new Map();
|
|
31
31
|
|
|
32
32
|
async store(token: StoredRefreshToken): Promise<void> {
|
|
33
|
-
this
|
|
33
|
+
this.#tokens.set(token.token, token);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async find(token: string): Promise<StoredRefreshToken | null> {
|
|
37
|
-
const stored = this
|
|
37
|
+
const stored = this.#tokens.get(token);
|
|
38
38
|
if (!stored) return null;
|
|
39
39
|
if (stored.expiresAt < Date.now()) {
|
|
40
|
-
this
|
|
40
|
+
this.#tokens.delete(token);
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
return stored;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async revoke(token: string): Promise<void> {
|
|
47
|
-
this
|
|
47
|
+
this.#tokens.delete(token);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async revokeAllForUser(userId: string | number): Promise<void> {
|
|
51
|
-
for (const [key, val] of this
|
|
52
|
-
if (val.userId === userId) this
|
|
51
|
+
for (const [key, val] of this.#tokens) {
|
|
52
|
+
if (val.userId === userId) this.#tokens.delete(key);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async cleanup(): Promise<void> {
|
|
57
57
|
const now = Date.now();
|
|
58
|
-
for (const [key, val] of this
|
|
59
|
-
if (val.expiresAt < now) this
|
|
58
|
+
for (const [key, val] of this.#tokens) {
|
|
59
|
+
if (val.expiresAt < now) this.#tokens.delete(key);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/TokenBlacklist.ts
CHANGED
|
@@ -18,17 +18,17 @@ export interface BlacklistDriver {
|
|
|
18
18
|
|
|
19
19
|
/** In-memory blacklist for development. */
|
|
20
20
|
export class MemoryBlacklistDriver implements BlacklistDriver {
|
|
21
|
-
|
|
21
|
+
#entries: Map<string, number> = new Map();
|
|
22
22
|
|
|
23
23
|
async add(jti: string, expiresAt: number): Promise<void> {
|
|
24
|
-
this
|
|
24
|
+
this.#entries.set(jti, expiresAt);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async has(jti: string): Promise<boolean> {
|
|
28
|
-
const expiresAt = this
|
|
28
|
+
const expiresAt = this.#entries.get(jti);
|
|
29
29
|
if (expiresAt === undefined) return false;
|
|
30
30
|
if (expiresAt < Date.now()) {
|
|
31
|
-
this
|
|
31
|
+
this.#entries.delete(jti);
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
return true;
|
|
@@ -36,8 +36,8 @@ export class MemoryBlacklistDriver implements BlacklistDriver {
|
|
|
36
36
|
|
|
37
37
|
async cleanup(): Promise<void> {
|
|
38
38
|
const now = Date.now();
|
|
39
|
-
for (const [jti, exp] of this
|
|
40
|
-
if (exp < now) this
|
|
39
|
+
for (const [jti, exp] of this.#entries) {
|
|
40
|
+
if (exp < now) this.#entries.delete(jti);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|