@boopkit/cap-users 0.7.0 → 0.8.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/README.md +14 -0
- package/dist/auth-middleware.d.ts +96 -2
- package/dist/auth-middleware.d.ts.map +1 -1
- package/dist/auth-middleware.js +131 -63
- package/dist/auth-middleware.js.map +1 -1
- package/dist/identity-service.d.ts +49 -0
- package/dist/identity-service.d.ts.map +1 -1
- package/dist/identity-service.js +34 -0
- package/dist/identity-service.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/reference/user-auth.ref.md +30 -0
- package/scaffold/src/types/express.ts +11 -1
package/README.md
CHANGED
|
@@ -132,6 +132,20 @@ router.get('/integrations', requireApiKey(), api.integrations); // an API key, n
|
|
|
132
132
|
|
|
133
133
|
`requireApiKey` accepts the request only when `resolveBearer` resolved it from a key (`req.authStrategy === 'api-key'`), then runs the same `isAuthorized` grant check `requireAuth` does — so an expired per-key grant is refused here even though the key still resolves.
|
|
134
134
|
|
|
135
|
+
**A bearer door of your own.** `resolveBearer` is the API-key instantiation of an exported factory, `createBearerMiddleware(deps, { resolve, strategy, challenge })`. A capability that issues its own kind of token — OAuth access tokens, say — supplies its own `resolve` callback and gets cap-users' bearer handling unchanged: the same header parsing, the same refusal semantics (401 with your challenge, never a fall-through to the session cookie), the same `req.user`/`req.sessionToken` fields downstream code already reads.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createBearerMiddleware } from '@boopkit/cap-users';
|
|
139
|
+
|
|
140
|
+
const resolveOauth = createBearerMiddleware(deps, {
|
|
141
|
+
resolve: (token) => tokenService.resolve(token), // null if it does not resolve
|
|
142
|
+
strategy: 'oauth', // written to req.authStrategy
|
|
143
|
+
challenge: 'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Give your path its **own** `strategy` label. `requireApiKey` gates on the `'api-key'` literal specifically, so reusing that label would walk your token through the one gate whose purpose is to demand an independently revocable API key. `req.authStrategy` is typed `string` for exactly this reason, while the gate's comparison stays on the literal.
|
|
148
|
+
|
|
135
149
|
Issue keys with `authService.createApiKey(userId)`, which returns the raw key once. Only `hashToken(apiKey)` is stored in `user_identities`, the same treatment a session cookie gets, so a read of that table hands nobody a working credential. Keys issued before this change were stored raw and will not resolve after it; reissue them.
|
|
136
150
|
|
|
137
151
|
There is deliberately no route, screen, or command for minting a key. Handing out a credential that acts as a user is not a generic operation — who may do it, for whom, whether it needs a second factor or an approval, and what the audit trail looks like are answers only your application has, and a default affordance would answer them for you on the sensitive side. Wire `createApiKey` behind whatever authorization you already trust, and tell the holder at the moment you show it that the key cannot be retrieved again.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth middleware factories for Express.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `createAuthMiddleware` returns the app's middleware bundle:
|
|
5
5
|
* - `resolveVisitor` — resolves req.user/sessionToken/visitorToken from auth_id/visitor_id
|
|
6
6
|
* cookies if present. Never creates a user or sets cookies. If no cookie resolves,
|
|
7
7
|
* req.user stays undefined so anonymous traffic does not mint user records.
|
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* Mints a user + visitor identity + session token and sets cookies on first call.
|
|
12
12
|
* Idempotent: if resolveVisitor already populated req, returns the existing user.
|
|
13
13
|
* - `requireAuth` — gates routes by trust tier, redirects web / 401s API.
|
|
14
|
+
* - `requireApiKey` — gates routes that must be reached with an API key specifically.
|
|
14
15
|
* - `addAuthContext` — populates res.locals.auth for view templates.
|
|
16
|
+
*
|
|
17
|
+
* `createBearerMiddleware` is the standalone bearer resolver those share. It
|
|
18
|
+
* takes the lookup as an injected callback, so a capability that issues its own
|
|
19
|
+
* kind of token (oauth-server's access tokens) gets cap-users' bearer handling
|
|
20
|
+
* — header parsing, the refusal semantics, the fields written onto the request
|
|
21
|
+
* — without cap-users having to know what an OAuth token is. `resolveBearer` is
|
|
22
|
+
* that factory with `validateApiKey` injected, which is what keeps the two
|
|
23
|
+
* doors from drifting apart.
|
|
15
24
|
*/
|
|
16
|
-
import type { Request, Response, NextFunction } from 'express';
|
|
25
|
+
import type { Request, RequestHandler, Response, NextFunction } from 'express';
|
|
17
26
|
import type { AuthService } from './auth-service.js';
|
|
18
27
|
import type { IdentityService } from './identity-service.js';
|
|
19
28
|
import type { UserService } from './user-service.js';
|
|
@@ -45,6 +54,42 @@ export interface AuthMiddlewareOptions {
|
|
|
45
54
|
*/
|
|
46
55
|
bearerChallenge?: string;
|
|
47
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Options for `createBearerMiddleware` — the three things that differ between
|
|
59
|
+
* one kind of bearer credential and another. Everything else about resolving a
|
|
60
|
+
* `Authorization: Bearer` header is the same for all of them, which is why they
|
|
61
|
+
* share an implementation.
|
|
62
|
+
*/
|
|
63
|
+
export interface BearerMiddlewareOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a presented token to its principal, or answer that it does not
|
|
66
|
+
* resolve. This is the whole identity domain in one callback: the caller
|
|
67
|
+
* decides which table the token lives in, what verification it needs, and
|
|
68
|
+
* which per-user conditions apply to it.
|
|
69
|
+
*
|
|
70
|
+
* Return `null` (or throw `AuthError`) for "this credential is not good" —
|
|
71
|
+
* both produce the 401. Throw anything else for "I could not tell" — a
|
|
72
|
+
* database being unreachable is not a bad token, and reporting it as one
|
|
73
|
+
* tells a correct client to discard a valid credential.
|
|
74
|
+
*/
|
|
75
|
+
resolve: (token: string) => Promise<{
|
|
76
|
+
user: User<BaseUserInfo>;
|
|
77
|
+
} | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Written to `req.authStrategy` on success — the label a downstream gate uses
|
|
80
|
+
* to tell one kind of credential from another. Use `'api-key'` only for an
|
|
81
|
+
* actual API key: `requireApiKey` gates on that exact literal, and reusing it
|
|
82
|
+
* for another credential walks that credential through the one gate whose
|
|
83
|
+
* purpose is to demand a key.
|
|
84
|
+
*/
|
|
85
|
+
strategy: string;
|
|
86
|
+
/**
|
|
87
|
+
* The `WWW-Authenticate` value sent with the 401. A plain `'Bearer'` for an
|
|
88
|
+
* API key; a resource server that must point clients at its discovery
|
|
89
|
+
* document sends the fuller `Bearer resource_metadata="..."` form.
|
|
90
|
+
*/
|
|
91
|
+
challenge: string;
|
|
92
|
+
}
|
|
48
93
|
/** Shape of res.locals.auth populated by addAuthContext. */
|
|
49
94
|
export interface AuthLocals {
|
|
50
95
|
trustTier: TrustTier | null;
|
|
@@ -61,6 +106,55 @@ export interface AuthMiddleware {
|
|
|
61
106
|
requireApiKey: (requiredTrust?: TrustTier) => (req: Request, res: Response, next: NextFunction) => void;
|
|
62
107
|
addAuthContext: (req: Request, res: Response, next: NextFunction) => void;
|
|
63
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* createBearerMiddleware — resolve `Authorization: Bearer <token>` through a
|
|
111
|
+
* caller-supplied lookup.
|
|
112
|
+
*
|
|
113
|
+
* Three outcomes, and the middle one is the design decision:
|
|
114
|
+
*
|
|
115
|
+
* - No `Authorization` header, or a scheme other than Bearer: pass through
|
|
116
|
+
* with req.user untouched, so this can be mounted on a router that also
|
|
117
|
+
* serves anonymous traffic. Gating is `requireAuth`'s job, not this one's.
|
|
118
|
+
* - A Bearer token that does not resolve: **401**, not silent anonymity. A
|
|
119
|
+
* caller that presented a credential and got it wrong should be told; a
|
|
120
|
+
* broken client that looks like a logged-out one is a bad afternoon.
|
|
121
|
+
* - A Bearer token that resolves: req.user is set to the same principal the
|
|
122
|
+
* cookie path yields, so everything downstream — requireAuth, trust tiers,
|
|
123
|
+
* res.locals.auth — works without knowing which door the request came in.
|
|
124
|
+
* It also records two more fields the cookie path does not:
|
|
125
|
+
* - req.sessionToken = the presented raw token. requireAuth reads BOTH
|
|
126
|
+
* req.user and req.sessionToken and denies if either is missing, so
|
|
127
|
+
* setting only req.user would make the published `resolveBearer` +
|
|
128
|
+
* `requireAuth` pairing deny every bearer request. On the API-key path
|
|
129
|
+
* sessionToken is also the `identityKey` requireAuth hands isAuthorized:
|
|
130
|
+
* `setAuthorization` hashes the key on the way in and `isAuthorized`
|
|
131
|
+
* hashes the presented one before comparing, so recording the RAW key
|
|
132
|
+
* here is what makes that comparison line up, and routing it through
|
|
133
|
+
* isAuthorized restores the per-key grant check ("expire the grant").
|
|
134
|
+
* - req.authStrategy = `options.strategy`, so a gate that must accept only
|
|
135
|
+
* one kind of credential (requireApiKey) can tell them apart. req.user
|
|
136
|
+
* alone cannot: resolveVisitor may have populated it from the cookie.
|
|
137
|
+
*
|
|
138
|
+
* It deliberately does **not** fall back to the session cookie when the token
|
|
139
|
+
* is bad. One of the donor codebases the oauth work drew on did fall back on
|
|
140
|
+
* a shared mount, and specs/oauth-server.spec.md names that as a shape not to
|
|
141
|
+
* lift: it means a request carrying a revoked key can still succeed, on the
|
|
142
|
+
* strength of a cookie the caller was not presenting as its credential. It is
|
|
143
|
+
* also why two bearer middlewares reading the same header is safe here and was
|
|
144
|
+
* not there — both refuse rather than fall through.
|
|
145
|
+
*
|
|
146
|
+
* Only outcomes that mean "this credential is not good" become 401s: a `null`
|
|
147
|
+
* from the resolver, or an `AuthError` thrown by it. Anything else (the
|
|
148
|
+
* database being unreachable, say) goes to next(err) so it is not misreported
|
|
149
|
+
* to the client as an auth failure.
|
|
150
|
+
*
|
|
151
|
+
* `deps` is not read here — resolution is entirely the caller's `resolve`
|
|
152
|
+
* callback — but it is part of the signature so that this factory and
|
|
153
|
+
* `createAuthMiddleware` are wired from the same object, and so a later
|
|
154
|
+
* addition that does need the services is not a breaking change for the
|
|
155
|
+
* capabilities already calling it.
|
|
156
|
+
*/
|
|
157
|
+
export declare function createBearerMiddleware(deps: AuthMiddlewareDeps, options: BearerMiddlewareOptions): RequestHandler;
|
|
64
158
|
/**
|
|
65
159
|
* Create auth middleware functions wired to the auth, identity, and user services.
|
|
66
160
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":"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"}
|
package/dist/auth-middleware.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Auth middleware factories for Express.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* `createAuthMiddleware` returns the app's middleware bundle:
|
|
6
6
|
* - `resolveVisitor` — resolves req.user/sessionToken/visitorToken from auth_id/visitor_id
|
|
7
7
|
* cookies if present. Never creates a user or sets cookies. If no cookie resolves,
|
|
8
8
|
* req.user stays undefined so anonymous traffic does not mint user records.
|
|
@@ -12,9 +12,19 @@
|
|
|
12
12
|
* Mints a user + visitor identity + session token and sets cookies on first call.
|
|
13
13
|
* Idempotent: if resolveVisitor already populated req, returns the existing user.
|
|
14
14
|
* - `requireAuth` — gates routes by trust tier, redirects web / 401s API.
|
|
15
|
+
* - `requireApiKey` — gates routes that must be reached with an API key specifically.
|
|
15
16
|
* - `addAuthContext` — populates res.locals.auth for view templates.
|
|
17
|
+
*
|
|
18
|
+
* `createBearerMiddleware` is the standalone bearer resolver those share. It
|
|
19
|
+
* takes the lookup as an injected callback, so a capability that issues its own
|
|
20
|
+
* kind of token (oauth-server's access tokens) gets cap-users' bearer handling
|
|
21
|
+
* — header parsing, the refusal semantics, the fields written onto the request
|
|
22
|
+
* — without cap-users having to know what an OAuth token is. `resolveBearer` is
|
|
23
|
+
* that factory with `validateApiKey` injected, which is what keeps the two
|
|
24
|
+
* doors from drifting apart.
|
|
16
25
|
*/
|
|
17
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.createBearerMiddleware = createBearerMiddleware;
|
|
18
28
|
exports.createAuthMiddleware = createAuthMiddleware;
|
|
19
29
|
const logger_1 = require("@boopkit/logger");
|
|
20
30
|
const auth_service_js_1 = require("./auth-service.js");
|
|
@@ -22,6 +32,93 @@ const logger = (0, logger_1.createLogger)('users', 'auth');
|
|
|
22
32
|
// ---------- Constants ----------
|
|
23
33
|
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
|
24
34
|
const TEN_YEARS_MS = 10 * ONE_YEAR_MS;
|
|
35
|
+
// ---------- Bearer factory ----------
|
|
36
|
+
/**
|
|
37
|
+
* createBearerMiddleware — resolve `Authorization: Bearer <token>` through a
|
|
38
|
+
* caller-supplied lookup.
|
|
39
|
+
*
|
|
40
|
+
* Three outcomes, and the middle one is the design decision:
|
|
41
|
+
*
|
|
42
|
+
* - No `Authorization` header, or a scheme other than Bearer: pass through
|
|
43
|
+
* with req.user untouched, so this can be mounted on a router that also
|
|
44
|
+
* serves anonymous traffic. Gating is `requireAuth`'s job, not this one's.
|
|
45
|
+
* - A Bearer token that does not resolve: **401**, not silent anonymity. A
|
|
46
|
+
* caller that presented a credential and got it wrong should be told; a
|
|
47
|
+
* broken client that looks like a logged-out one is a bad afternoon.
|
|
48
|
+
* - A Bearer token that resolves: req.user is set to the same principal the
|
|
49
|
+
* cookie path yields, so everything downstream — requireAuth, trust tiers,
|
|
50
|
+
* res.locals.auth — works without knowing which door the request came in.
|
|
51
|
+
* It also records two more fields the cookie path does not:
|
|
52
|
+
* - req.sessionToken = the presented raw token. requireAuth reads BOTH
|
|
53
|
+
* req.user and req.sessionToken and denies if either is missing, so
|
|
54
|
+
* setting only req.user would make the published `resolveBearer` +
|
|
55
|
+
* `requireAuth` pairing deny every bearer request. On the API-key path
|
|
56
|
+
* sessionToken is also the `identityKey` requireAuth hands isAuthorized:
|
|
57
|
+
* `setAuthorization` hashes the key on the way in and `isAuthorized`
|
|
58
|
+
* hashes the presented one before comparing, so recording the RAW key
|
|
59
|
+
* here is what makes that comparison line up, and routing it through
|
|
60
|
+
* isAuthorized restores the per-key grant check ("expire the grant").
|
|
61
|
+
* - req.authStrategy = `options.strategy`, so a gate that must accept only
|
|
62
|
+
* one kind of credential (requireApiKey) can tell them apart. req.user
|
|
63
|
+
* alone cannot: resolveVisitor may have populated it from the cookie.
|
|
64
|
+
*
|
|
65
|
+
* It deliberately does **not** fall back to the session cookie when the token
|
|
66
|
+
* is bad. One of the donor codebases the oauth work drew on did fall back on
|
|
67
|
+
* a shared mount, and specs/oauth-server.spec.md names that as a shape not to
|
|
68
|
+
* lift: it means a request carrying a revoked key can still succeed, on the
|
|
69
|
+
* strength of a cookie the caller was not presenting as its credential. It is
|
|
70
|
+
* also why two bearer middlewares reading the same header is safe here and was
|
|
71
|
+
* not there — both refuse rather than fall through.
|
|
72
|
+
*
|
|
73
|
+
* Only outcomes that mean "this credential is not good" become 401s: a `null`
|
|
74
|
+
* from the resolver, or an `AuthError` thrown by it. Anything else (the
|
|
75
|
+
* database being unreachable, say) goes to next(err) so it is not misreported
|
|
76
|
+
* to the client as an auth failure.
|
|
77
|
+
*
|
|
78
|
+
* `deps` is not read here — resolution is entirely the caller's `resolve`
|
|
79
|
+
* callback — but it is part of the signature so that this factory and
|
|
80
|
+
* `createAuthMiddleware` are wired from the same object, and so a later
|
|
81
|
+
* addition that does need the services is not a breaking change for the
|
|
82
|
+
* capabilities already calling it.
|
|
83
|
+
*/
|
|
84
|
+
function createBearerMiddleware(deps, options) {
|
|
85
|
+
const { resolve, strategy, challenge } = options;
|
|
86
|
+
void deps;
|
|
87
|
+
return function resolveBearerToken(req, res, next) {
|
|
88
|
+
const header = req.headers?.authorization;
|
|
89
|
+
if (typeof header !== 'string')
|
|
90
|
+
return next();
|
|
91
|
+
const match = /^Bearer +(.+)$/i.exec(header.trim());
|
|
92
|
+
if (!match)
|
|
93
|
+
return next();
|
|
94
|
+
const token = match[1].trim();
|
|
95
|
+
if (token === '')
|
|
96
|
+
return next();
|
|
97
|
+
const refuse = () => {
|
|
98
|
+
logger.warn('bearer token refused', { path: req.path, strategy });
|
|
99
|
+
res.set('WWW-Authenticate', challenge);
|
|
100
|
+
res.status(401).json({ error: 'invalid_token' });
|
|
101
|
+
};
|
|
102
|
+
// Promise.resolve().then wraps a resolver that throws synchronously, so a
|
|
103
|
+
// buggy caller cannot take the process down instead of reaching next(err).
|
|
104
|
+
Promise.resolve()
|
|
105
|
+
.then(() => resolve(token))
|
|
106
|
+
.then((resolved) => {
|
|
107
|
+
if (!resolved)
|
|
108
|
+
return refuse();
|
|
109
|
+
const r = req;
|
|
110
|
+
r.user = resolved.user;
|
|
111
|
+
r.sessionToken = token;
|
|
112
|
+
r.authStrategy = strategy;
|
|
113
|
+
next();
|
|
114
|
+
})
|
|
115
|
+
.catch((err) => {
|
|
116
|
+
if (!(err instanceof auth_service_js_1.AuthError))
|
|
117
|
+
return next(err);
|
|
118
|
+
refuse();
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
}
|
|
25
122
|
// ---------- Factory ----------
|
|
26
123
|
/**
|
|
27
124
|
* Create auth middleware functions wired to the auth, identity, and user services.
|
|
@@ -45,73 +142,38 @@ function createAuthMiddleware(deps, options) {
|
|
|
45
142
|
resolveFromCookies(req).then(() => next()).catch(next);
|
|
46
143
|
}
|
|
47
144
|
/**
|
|
48
|
-
* resolveBearer —
|
|
145
|
+
* resolveBearer — the API-key instantiation of `createBearerMiddleware`.
|
|
49
146
|
*
|
|
50
|
-
*
|
|
147
|
+
* This is not a convenience wrapper around the general factory; it is the
|
|
148
|
+
* whole of the API-key bearer path. The spec's non-negotiable constraint for
|
|
149
|
+
* the OAuth widening was that the API-key path keep behaving exactly as it
|
|
150
|
+
* did, and building it out of the same factory the OAuth path uses makes that
|
|
151
|
+
* true by construction rather than by inspection: there is one bearer
|
|
152
|
+
* implementation, and every existing API-key test exercises it. Two parallel
|
|
153
|
+
* implementations that are supposed to agree is the arrangement that drifts.
|
|
51
154
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* serves anonymous traffic. Gating is `requireAuth`'s job, not this one's.
|
|
55
|
-
* - A Bearer token that does not resolve: **401**, not silent anonymity. A
|
|
56
|
-
* caller that presented a credential and got it wrong should be told; a
|
|
57
|
-
* broken client that looks like a logged-out one is a bad afternoon.
|
|
58
|
-
* - A Bearer token that resolves: req.user is set to the same principal the
|
|
59
|
-
* cookie path yields, so everything downstream — requireAuth, trust tiers,
|
|
60
|
-
* res.locals.auth — works without knowing which door the request came in.
|
|
61
|
-
* It also records two more fields the cookie path does not:
|
|
62
|
-
* - req.sessionToken = the presented raw key. requireAuth reads BOTH
|
|
63
|
-
* req.user and req.sessionToken and denies if either is missing, so
|
|
64
|
-
* setting only req.user would make the published `resolveBearer` +
|
|
65
|
-
* `requireAuth` pairing deny every bearer request. sessionToken is also
|
|
66
|
-
* the `identityKey` requireAuth hands isAuthorized, and createApiKey
|
|
67
|
-
* records the per-key grant under the RAW key — so the presented key is
|
|
68
|
-
* exactly what that call is asking for, and routing it through
|
|
69
|
-
* isAuthorized restores the per-key grant check ("expire the grant") for
|
|
70
|
-
* free. COUPLING: this relies on createApiKey keying the grant on the
|
|
71
|
-
* raw key (auth-service.ts, `setAuthorization(userId, apiKey, ...)`).
|
|
72
|
-
* boop-owbv proposes hashing that field; whichever of the two lands
|
|
73
|
-
* second MUST update the other or API-key authorization breaks silently.
|
|
74
|
-
* - req.authStrategy = 'api-key', so a gate that must accept ONLY a key
|
|
75
|
-
* (requireApiKey) can tell a key principal from a cookie one. req.user
|
|
76
|
-
* alone cannot: resolveVisitor may have populated it from the cookie.
|
|
155
|
+
* The three injected pieces are the whole of what makes this path the
|
|
156
|
+
* API-key one:
|
|
77
157
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
158
|
+
* - `validateApiKey` as the resolver. It does four things an OAuth token
|
|
159
|
+
* would fail three of — the hardcoded `'api-key'` identity domain, a bcrypt
|
|
160
|
+
* check against the user's `credentials` property, an
|
|
161
|
+
* `isMethodAccepted(userId, 'api-key')` check against their `authentication`
|
|
162
|
+
* property, and the lookup itself — which is exactly why the seam is a
|
|
163
|
+
* callback rather than a `domain` option. It signals a bad key by throwing
|
|
164
|
+
* `AuthError`, which the factory treats as a refusal.
|
|
165
|
+
* - `'api-key'` as the strategy label, the literal `requireApiKey` gates on.
|
|
166
|
+
* - `bearerChallenge` as the `WWW-Authenticate` value, so an app can still
|
|
167
|
+
* override the challenge through `createAuthMiddleware`'s options.
|
|
83
168
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* misreported to the client as an auth failure.
|
|
169
|
+
* See `createBearerMiddleware` above for the resolution semantics and for why
|
|
170
|
+
* a bad token refuses rather than falling through to the session cookie.
|
|
87
171
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!match)
|
|
94
|
-
return next();
|
|
95
|
-
const token = match[1].trim();
|
|
96
|
-
if (token === '')
|
|
97
|
-
return next();
|
|
98
|
-
authService
|
|
99
|
-
.validateApiKey(token)
|
|
100
|
-
.then((user) => {
|
|
101
|
-
const r = req;
|
|
102
|
-
r.user = user;
|
|
103
|
-
r.sessionToken = token;
|
|
104
|
-
r.authStrategy = 'api-key';
|
|
105
|
-
next();
|
|
106
|
-
})
|
|
107
|
-
.catch((err) => {
|
|
108
|
-
if (!(err instanceof auth_service_js_1.AuthError))
|
|
109
|
-
return next(err);
|
|
110
|
-
logger.warn('bearer token refused', { path: req.path });
|
|
111
|
-
res.set('WWW-Authenticate', bearerChallenge);
|
|
112
|
-
res.status(401).json({ error: 'invalid_token' });
|
|
113
|
-
});
|
|
114
|
-
}
|
|
172
|
+
const resolveBearer = createBearerMiddleware(deps, {
|
|
173
|
+
resolve: (token) => authService.validateApiKey(token).then((user) => ({ user })),
|
|
174
|
+
strategy: 'api-key',
|
|
175
|
+
challenge: bearerChallenge,
|
|
176
|
+
});
|
|
115
177
|
async function resolveFromCookies(req) {
|
|
116
178
|
let user;
|
|
117
179
|
let sessionToken;
|
|
@@ -221,6 +283,12 @@ function createAuthMiddleware(deps, options) {
|
|
|
221
283
|
const user = req.user;
|
|
222
284
|
const sessionToken = req.sessionToken;
|
|
223
285
|
const authStrategy = req.authStrategy;
|
|
286
|
+
// The comparison stays on the 'api-key' LITERAL even though the field's
|
|
287
|
+
// declared type widened to string. That is the point of the widening:
|
|
288
|
+
// another capability may label its own bearer path 'oauth', and this gate
|
|
289
|
+
// exists to demand an independently revocable API key specifically, so an
|
|
290
|
+
// OAuth access token has to fail here by construction rather than by
|
|
291
|
+
// somebody remembering to check.
|
|
224
292
|
if (!user || !sessionToken || authStrategy !== 'api-key') {
|
|
225
293
|
res.set('WWW-Authenticate', bearerChallenge).status(401).json({ error: 'Unauthorized' });
|
|
226
294
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middleware.js","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"auth-middleware.js","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,12 +9,57 @@ export interface IdentityServiceOptions {
|
|
|
9
9
|
export interface IdentityQueryOptions {
|
|
10
10
|
includeEnded?: boolean;
|
|
11
11
|
}
|
|
12
|
+
/** Rows per page when a key-prefix scan is not told otherwise. */
|
|
13
|
+
export declare const IDENTITY_PREFIX_PAGE_SIZE = 100;
|
|
14
|
+
/** The most a single page of a key-prefix scan will return, however asked. */
|
|
15
|
+
export declare const IDENTITY_PREFIX_MAX_PAGE_SIZE = 500;
|
|
16
|
+
/** Options for the key-prefix scan. */
|
|
17
|
+
export interface IdentityPrefixQueryOptions extends IdentityQueryOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Rows in this page. Defaults to `IDENTITY_PREFIX_PAGE_SIZE` and is clamped
|
|
20
|
+
* to `IDENTITY_PREFIX_MAX_PAGE_SIZE`, so a caller cannot turn the scan into
|
|
21
|
+
* the whole-table read it exists to avoid.
|
|
22
|
+
*/
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** Exclusive cursor — the previous page's `nextCursor`. */
|
|
25
|
+
after?: number;
|
|
26
|
+
}
|
|
27
|
+
/** One page of a key-prefix scan. */
|
|
28
|
+
export interface IdentityPrefixPage {
|
|
29
|
+
identities: UserIdentity[];
|
|
30
|
+
/**
|
|
31
|
+
* Feed back as `after` to continue. `null` when this page ended the scan.
|
|
32
|
+
*
|
|
33
|
+
* A full page always answers with a cursor, even when it happened to be the
|
|
34
|
+
* last one, so the caller's final request is an empty page rather than a
|
|
35
|
+
* count the query cannot know without a second read.
|
|
36
|
+
*/
|
|
37
|
+
nextCursor: number | null;
|
|
38
|
+
}
|
|
12
39
|
/** The service object returned by createIdentityService. */
|
|
13
40
|
export interface IdentityService {
|
|
14
41
|
create(userId: number, domain: string, key: string): Promise<UserIdentity>;
|
|
15
42
|
findByDomainAndKey(domain: string, key: string): Promise<UserIdentity | null>;
|
|
16
43
|
findByUserAndDomain(userId: number, domain: string): Promise<UserIdentity | null>;
|
|
17
44
|
findAllByUser(userId: number, opts?: IdentityQueryOptions): Promise<UserIdentity[]>;
|
|
45
|
+
/**
|
|
46
|
+
* One page of the rows in `domain` whose key starts with `keyPrefix`, in id
|
|
47
|
+
* order, for any user.
|
|
48
|
+
*
|
|
49
|
+
* This is the only read here that is not bounded by a user id, and it exists
|
|
50
|
+
* for one job: a credential domain whose keys are `"<issuer>|<secret hash>"`
|
|
51
|
+
* — cap-oauth-server's token rows — needs to enumerate everybody one issuer
|
|
52
|
+
* ever reached, so that a compromised client can be retired without the
|
|
53
|
+
* operator already knowing which accounts to sweep.
|
|
54
|
+
*
|
|
55
|
+
* Both narrowings are load-bearing rather than conveniences. The **domain is
|
|
56
|
+
* required** so the scan cannot wander into `email` or `api-key` rows, and an
|
|
57
|
+
* **empty prefix is refused** because it would walk a whole domain — which is
|
|
58
|
+
* the read this method is shaped to make unnecessary. Left-anchored, the
|
|
59
|
+
* comparison is a seek on the `(domain, key)` unique index rather than a
|
|
60
|
+
* table scan, which is the whole reason the key format puts the issuer first.
|
|
61
|
+
*/
|
|
62
|
+
findAllByDomainAndKeyPrefix(domain: string, keyPrefix: string, opts?: IdentityPrefixQueryOptions): Promise<IdentityPrefixPage>;
|
|
18
63
|
update(domain: string, key: string, newUserId: number): Promise<UserIdentity>;
|
|
19
64
|
endDate(domain: string, key: string): Promise<UserIdentity>;
|
|
20
65
|
}
|
|
@@ -31,6 +76,10 @@ interface IdentityDelegate {
|
|
|
31
76
|
}): Promise<Record<string, unknown> | null>;
|
|
32
77
|
findMany(args: {
|
|
33
78
|
where: Record<string, unknown>;
|
|
79
|
+
orderBy?: {
|
|
80
|
+
id?: 'asc' | 'desc';
|
|
81
|
+
};
|
|
82
|
+
take?: number;
|
|
34
83
|
}): Promise<Record<string, unknown>[]>;
|
|
35
84
|
update(args: {
|
|
36
85
|
where: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-service.d.ts","sourceRoot":"","sources":["../src/identity-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IACzC,OAAO,CAAC,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;CACxC;AAED,6DAA6D;AAC7D,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3E,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC9E,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAClF,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IACpF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9E,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC7D;AAED,kEAAkE;AAClE,UAAU,gBAAgB;IACxB,MAAM,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClF,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9F,SAAS,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7F,QAAQ,CAAC,IAAI,EAAE;
|
|
1
|
+
{"version":3,"file":"identity-service.d.ts","sourceRoot":"","sources":["../src/identity-service.ts"],"names":[],"mappings":"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"}
|
package/dist/identity-service.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_PREFIX_MAX_PAGE_SIZE = exports.IDENTITY_PREFIX_PAGE_SIZE = void 0;
|
|
3
4
|
exports.createIdentityService = createIdentityService;
|
|
4
5
|
const types_js_1 = require("./types.js");
|
|
6
|
+
/** Rows per page when a key-prefix scan is not told otherwise. */
|
|
7
|
+
exports.IDENTITY_PREFIX_PAGE_SIZE = 100;
|
|
8
|
+
/** The most a single page of a key-prefix scan will return, however asked. */
|
|
9
|
+
exports.IDENTITY_PREFIX_MAX_PAGE_SIZE = 500;
|
|
5
10
|
function toIdentity(row) {
|
|
6
11
|
const endedAt = row.endedAt;
|
|
7
12
|
return {
|
|
@@ -75,6 +80,35 @@ function createIdentityService(model, options) {
|
|
|
75
80
|
const rows = await model.findMany({ where });
|
|
76
81
|
return rows.map(toIdentity);
|
|
77
82
|
},
|
|
83
|
+
async findAllByDomainAndKeyPrefix(domain, keyPrefix, opts) {
|
|
84
|
+
if (!domain) {
|
|
85
|
+
throw new TypeError('Unable to scan identities by key prefix: no domain was given. ' +
|
|
86
|
+
'Pass the identity domain to scan, such as "oauth-access".');
|
|
87
|
+
}
|
|
88
|
+
if (!keyPrefix) {
|
|
89
|
+
throw new TypeError(`Unable to scan identities by key prefix: the prefix was empty, which would read every "${domain}" row. ` +
|
|
90
|
+
'Pass the prefix to match, or use findAllByUser for one user\'s identities.');
|
|
91
|
+
}
|
|
92
|
+
const limit = Math.min(Math.max(1, Math.trunc(opts?.limit ?? exports.IDENTITY_PREFIX_PAGE_SIZE)), exports.IDENTITY_PREFIX_MAX_PAGE_SIZE);
|
|
93
|
+
const where = {
|
|
94
|
+
domain,
|
|
95
|
+
key: { startsWith: normalizeKey(domain, keyPrefix) },
|
|
96
|
+
};
|
|
97
|
+
if (!opts?.includeEnded) {
|
|
98
|
+
where.endedAt = null;
|
|
99
|
+
}
|
|
100
|
+
// The cursor is the row id rather than an offset: pages are stable under
|
|
101
|
+
// rows arriving mid-scan, which for a token table is every request.
|
|
102
|
+
if (opts?.after != null) {
|
|
103
|
+
where.id = { gt: opts.after };
|
|
104
|
+
}
|
|
105
|
+
const rows = await model.findMany({ where, orderBy: { id: 'asc' }, take: limit });
|
|
106
|
+
const identities = rows.map(toIdentity);
|
|
107
|
+
return {
|
|
108
|
+
identities,
|
|
109
|
+
nextCursor: identities.length === limit ? identities[identities.length - 1].id : null,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
78
112
|
async update(domain, key, newUserId) {
|
|
79
113
|
const row = await model.update({
|
|
80
114
|
where: domainKeyWhere(domain, normalizeKey(domain, key)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-service.js","sourceRoot":"","sources":["../src/identity-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"identity-service.js","sourceRoot":"","sources":["../src/identity-service.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,13 +9,14 @@ export type { BaseUserInfo, User, UserIdentity, UserProfile, CredentialEntry, Us
|
|
|
9
9
|
export { createUserService } from './user-service.js';
|
|
10
10
|
export type { UserService, UserServiceOptions } from './user-service.js';
|
|
11
11
|
export { createIdentityService } from './identity-service.js';
|
|
12
|
-
export
|
|
12
|
+
export { IDENTITY_PREFIX_PAGE_SIZE, IDENTITY_PREFIX_MAX_PAGE_SIZE, } from './identity-service.js';
|
|
13
|
+
export type { IdentityService, IdentityServiceOptions, IdentityQueryOptions, IdentityPrefixQueryOptions, IdentityPrefixPage, } from './identity-service.js';
|
|
13
14
|
export { createUserPropertyService } from './user-property-service.js';
|
|
14
15
|
export type { UserPropertyService, UserPropertyServiceOptions } from './user-property-service.js';
|
|
15
16
|
export { createAuthService, AuthValidationError, AuthError, generateToken, hashToken, isHashedToken } from './auth-service.js';
|
|
16
17
|
export type { AuthService, AuthServiceDeps, AuthServiceOptions, AddAuthMethodOptions, SignupInput, SignupResult, LoginResult, VerifyEmailResult, } from './auth-service.js';
|
|
17
|
-
export { createAuthMiddleware } from './auth-middleware.js';
|
|
18
|
-
export type { AuthMiddleware, AuthMiddlewareDeps, AuthMiddlewareOptions, AuthLocals, } from './auth-middleware.js';
|
|
18
|
+
export { createAuthMiddleware, createBearerMiddleware } from './auth-middleware.js';
|
|
19
|
+
export type { AuthMiddleware, AuthMiddlewareDeps, AuthMiddlewareOptions, BearerMiddlewareOptions, AuthLocals, } from './auth-middleware.js';
|
|
19
20
|
export { createAccountService } from './account-service.js';
|
|
20
21
|
export type { AccountService, AccountServiceOptions } from './account-service.js';
|
|
21
22
|
export { createAccountPropertyService } from './account-property-service.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EACV,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EACV,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAElG,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC/H,YAAY,EACV,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACpF,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACvB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAC7E,YAAY,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAE3G,OAAO,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzG,YAAY,EACV,wBAAwB,EACxB,4BAA4B,EAC5B,+BAA+B,EAC/B,qBAAqB,EACrB,sBAAsB,EACtB,UAAU,GACX,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,GACZ,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,cAAc,EACd,2BAA2B,EAC3B,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,EAC5B,eAAe,GAChB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,gCAAgC,EAChC,mCAAmC,GACpC,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,eAAe,GAChB,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* composing on top of @boopkit/cap-services primitives.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.teamInvitationsStore = exports.rehashTeamInvitations = exports.authorizationStore = exports.rehashAuthorizationGrants = exports.migrateCredentialStore = exports.TEAM_INVITATIONS_HASHED_INFO_SCHEMA = exports.AUTHORIZATION_HASHED_INFO_SCHEMA = exports.MAX_RESENDS = exports.MAX_PENDING_INVITATIONS = exports.DEFAULT_INVITATION_TTL_MS = exports.TeamInvitationError = exports.createTeamInvitationService = exports.AccountManagementError = exports.createAccountManagementService = exports.createAccountPropertyService = exports.createAccountService = exports.createAuthMiddleware = exports.isHashedToken = exports.hashToken = exports.generateToken = exports.AuthError = exports.AuthValidationError = exports.createAuthService = exports.createUserPropertyService = exports.createIdentityService = exports.createUserService = exports.DuplicateAccountUserError = exports.DuplicateIdentityError = void 0;
|
|
9
|
+
exports.teamInvitationsStore = exports.rehashTeamInvitations = exports.authorizationStore = exports.rehashAuthorizationGrants = exports.migrateCredentialStore = exports.TEAM_INVITATIONS_HASHED_INFO_SCHEMA = exports.AUTHORIZATION_HASHED_INFO_SCHEMA = exports.MAX_RESENDS = exports.MAX_PENDING_INVITATIONS = exports.DEFAULT_INVITATION_TTL_MS = exports.TeamInvitationError = exports.createTeamInvitationService = exports.AccountManagementError = exports.createAccountManagementService = exports.createAccountPropertyService = exports.createAccountService = exports.createBearerMiddleware = exports.createAuthMiddleware = exports.isHashedToken = exports.hashToken = exports.generateToken = exports.AuthError = exports.AuthValidationError = exports.createAuthService = exports.createUserPropertyService = exports.IDENTITY_PREFIX_MAX_PAGE_SIZE = exports.IDENTITY_PREFIX_PAGE_SIZE = exports.createIdentityService = exports.createUserService = exports.DuplicateAccountUserError = exports.DuplicateIdentityError = void 0;
|
|
10
10
|
var types_js_1 = require("./types.js");
|
|
11
11
|
Object.defineProperty(exports, "DuplicateIdentityError", { enumerable: true, get: function () { return types_js_1.DuplicateIdentityError; } });
|
|
12
12
|
Object.defineProperty(exports, "DuplicateAccountUserError", { enumerable: true, get: function () { return types_js_1.DuplicateAccountUserError; } });
|
|
@@ -14,6 +14,9 @@ var user_service_js_1 = require("./user-service.js");
|
|
|
14
14
|
Object.defineProperty(exports, "createUserService", { enumerable: true, get: function () { return user_service_js_1.createUserService; } });
|
|
15
15
|
var identity_service_js_1 = require("./identity-service.js");
|
|
16
16
|
Object.defineProperty(exports, "createIdentityService", { enumerable: true, get: function () { return identity_service_js_1.createIdentityService; } });
|
|
17
|
+
var identity_service_js_2 = require("./identity-service.js");
|
|
18
|
+
Object.defineProperty(exports, "IDENTITY_PREFIX_PAGE_SIZE", { enumerable: true, get: function () { return identity_service_js_2.IDENTITY_PREFIX_PAGE_SIZE; } });
|
|
19
|
+
Object.defineProperty(exports, "IDENTITY_PREFIX_MAX_PAGE_SIZE", { enumerable: true, get: function () { return identity_service_js_2.IDENTITY_PREFIX_MAX_PAGE_SIZE; } });
|
|
17
20
|
var user_property_service_js_1 = require("./user-property-service.js");
|
|
18
21
|
Object.defineProperty(exports, "createUserPropertyService", { enumerable: true, get: function () { return user_property_service_js_1.createUserPropertyService; } });
|
|
19
22
|
var auth_service_js_1 = require("./auth-service.js");
|
|
@@ -25,6 +28,7 @@ Object.defineProperty(exports, "hashToken", { enumerable: true, get: function ()
|
|
|
25
28
|
Object.defineProperty(exports, "isHashedToken", { enumerable: true, get: function () { return auth_service_js_1.isHashedToken; } });
|
|
26
29
|
var auth_middleware_js_1 = require("./auth-middleware.js");
|
|
27
30
|
Object.defineProperty(exports, "createAuthMiddleware", { enumerable: true, get: function () { return auth_middleware_js_1.createAuthMiddleware; } });
|
|
31
|
+
Object.defineProperty(exports, "createBearerMiddleware", { enumerable: true, get: function () { return auth_middleware_js_1.createBearerMiddleware; } });
|
|
28
32
|
var account_service_js_1 = require("./account-service.js");
|
|
29
33
|
Object.defineProperty(exports, "createAccountService", { enumerable: true, get: function () { return account_service_js_1.createAccountService; } });
|
|
30
34
|
var account_property_service_js_1 = require("./account-property-service.js");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,uCAA+E;AAAtE,kHAAA,sBAAsB,OAAA;AAAE,qHAAA,yBAAyB,OAAA;AAmB1D,qDAAsD;AAA7C,oHAAA,iBAAiB,OAAA;AAG1B,6DAA8D;AAArD,4HAAA,qBAAqB,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,uCAA+E;AAAtE,kHAAA,sBAAsB,OAAA;AAAE,qHAAA,yBAAyB,OAAA;AAmB1D,qDAAsD;AAA7C,oHAAA,iBAAiB,OAAA;AAG1B,6DAA8D;AAArD,4HAAA,qBAAqB,OAAA;AAC9B,6DAG+B;AAF7B,gIAAA,yBAAyB,OAAA;AACzB,oIAAA,6BAA6B,OAAA;AAU/B,uEAAuE;AAA9D,qIAAA,yBAAyB,OAAA;AAGlC,qDAA+H;AAAtH,oHAAA,iBAAiB,OAAA;AAAE,sHAAA,mBAAmB,OAAA;AAAE,4GAAA,SAAS,OAAA;AAAE,gHAAA,aAAa,OAAA;AAAE,4GAAA,SAAS,OAAA;AAAE,gHAAA,aAAa,OAAA;AAYnG,2DAAoF;AAA3E,0HAAA,oBAAoB,OAAA;AAAE,4HAAA,sBAAsB,OAAA;AASrD,2DAA4D;AAAnD,0HAAA,oBAAoB,OAAA;AAG7B,6EAA6E;AAApE,2IAAA,4BAA4B,OAAA;AAGrC,iFAAyG;AAAhG,+IAAA,8BAA8B,OAAA;AAAE,uIAAA,sBAAsB,OAAA;AAU/D,2EAMsC;AALpC,yIAAA,2BAA2B,OAAA;AAC3B,iIAAA,mBAAmB,OAAA;AACnB,uIAAA,yBAAyB,OAAA;AACzB,qIAAA,uBAAuB,OAAA;AACvB,yHAAA,WAAW,OAAA;AAWb,6DAG+B;AAF7B,uIAAA,gCAAgC,OAAA;AAChC,0IAAA,mCAAmC,OAAA;AAGrC,qEAMmC;AALjC,iIAAA,sBAAsB,OAAA;AACtB,oIAAA,yBAAyB,OAAA;AACzB,6HAAA,kBAAkB,OAAA;AAClB,gIAAA,qBAAqB,OAAA;AACrB,+HAAA,oBAAoB,OAAA"}
|
package/package.json
CHANGED
|
@@ -159,6 +159,10 @@ A request with no `Authorization` header passes through with `req.user` unset, s
|
|
|
159
159
|
A token that does not resolve gets `401 { error: 'invalid_token' }` and a `WWW-Authenticate` challenge, and does **not** fall back to the session cookie — revoking a key has to stop the request even from a browser that is still logged in.
|
|
160
160
|
It refuses a malformed token on every route it covers, open ones included, which is why the health check is mounted ahead of it: a route registered before `resolveBearer` never sees the middleware, so a monitor carrying a stale or garbled `Authorization` header still gets its `200` rather than a `401`.
|
|
161
161
|
|
|
162
|
+
`resolveBearer` is itself `createBearerMiddleware(deps, { resolve, strategy, challenge })` — exported from `@boopkit/cap-users` — with `validateApiKey` as the resolver and `'api-key'` as the strategy.
|
|
163
|
+
If your app issues another kind of bearer token, build a second middleware from that factory rather than writing one: you get the same parsing and the same refusal semantics, and both doors refuse instead of falling through to the cookie, which is what makes two of them safe on one header.
|
|
164
|
+
Give it its own strategy label — `req.authStrategy` is typed `string` so you can — because `requireApiKey` gates on the `'api-key'` literal, and reusing that label would let your token through the one gate that exists to demand a key.
|
|
165
|
+
|
|
162
166
|
**Issuing a key is yours to design.**
|
|
163
167
|
`authService.createApiKey(userId)` mints one and returns the raw key exactly once; the identity row holds its SHA-256 and the credentials entry a bcrypt hash, so nothing can hand it back later and any surface you build has to say so at the moment it is shown.
|
|
164
168
|
This capability deliberately ships no route, screen, or command for it.
|
|
@@ -242,6 +246,32 @@ All services compose from `@boopkit/cap-services` primitives:
|
|
|
242
246
|
| `userPropertyService` | `createUserPropertyService()` | Read/write user properties (credentials gated) |
|
|
243
247
|
| `authService` | `createAuthService(deps, opts)` | Orchestrates signup, login, sessions, API keys |
|
|
244
248
|
|
|
249
|
+
### IdentityService reads
|
|
250
|
+
|
|
251
|
+
Every read is bounded by a user id or an exact `(domain, key)` pair — except one.
|
|
252
|
+
|
|
253
|
+
`findAllByDomainAndKeyPrefix(domain, keyPrefix, opts?)` returns one page of the rows in a single
|
|
254
|
+
domain whose key starts with `keyPrefix`, for **any** user, in id order:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const page = await identityService.findAllByDomainAndKeyPrefix('oauth-access', `${publicId}|`, {
|
|
258
|
+
includeEnded: true,
|
|
259
|
+
limit: 100, // clamped to 500
|
|
260
|
+
after: previousPage.nextCursor ?? undefined,
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
It exists for credential domains whose keys are `"<issuer>|<secret hash>"` — cap-oauth-server's
|
|
265
|
+
token rows — where retiring one issuer means finding everybody it ever reached, and the operator
|
|
266
|
+
should not have to know the blast radius in advance. Left-anchored, the comparison is a seek on the
|
|
267
|
+
`(domain, key)` unique index rather than a table scan.
|
|
268
|
+
|
|
269
|
+
Both narrowings are load-bearing rather than ergonomic. The **domain is required**, so the scan
|
|
270
|
+
cannot wander into `email` or `api-key` rows; an **empty prefix throws**, because it would walk a
|
|
271
|
+
whole domain, which is the read this method exists to make unnecessary. It **pages** rather than
|
|
272
|
+
materialising a result set of unknown size: keep passing `nextCursor` back as `after` until it comes
|
|
273
|
+
back `null`.
|
|
274
|
+
|
|
245
275
|
### AuthService key operations
|
|
246
276
|
|
|
247
277
|
**Signup**: `signup({ email, password, name })` — creates user, email identity, profile, credentials, auth method, session token, and authorization grant in one call. Each step is also available individually for graduated control.
|
|
@@ -60,8 +60,18 @@ declare global {
|
|
|
60
60
|
* the session cookie via resolveVisitor. A gate that must accept only one
|
|
61
61
|
* kind of credential (requireApiKey) reads this — req.user alone cannot
|
|
62
62
|
* tell a key principal from a cookie one.
|
|
63
|
+
*
|
|
64
|
+
* `string`, not the `'api-key'` literal, because the label is chosen by
|
|
65
|
+
* whoever built the bearer middleware: `createBearerMiddleware` writes
|
|
66
|
+
* whatever `options.strategy` it was given, so a capability that issues
|
|
67
|
+
* its own tokens labels them its own way (oauth-server uses `'oauth'`).
|
|
68
|
+
* cap-users' own `resolveBearer` still writes `'api-key'`, and
|
|
69
|
+
* `requireApiKey` still compares against that literal — which is what
|
|
70
|
+
* makes another capability's token fail a key-only gate rather than pass
|
|
71
|
+
* it. Widening the label and widening the gate are NOT the same change;
|
|
72
|
+
* only the label widens.
|
|
63
73
|
*/
|
|
64
|
-
authStrategy?:
|
|
74
|
+
authStrategy?: string;
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
}
|