@occupop/lib-auth 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,20 @@ ENV variables required
27
27
  JWT_SECRET=randomTokenProvidedByAuthService
28
28
  ```
29
29
 
30
+ ## Compatibility
31
+
32
+ - **Node:** an ES module package (`"type": "module"`), loadable with `import` and with `require()`.
33
+ `require()` of an ES module needs `require(esm)`, which is on by default since Node 20.19 and
34
+ 22.12; Node 22 and 24 are the versions tested. Since 4.1.0 the package takes `jsonwebtoken`, a
35
+ CommonJS module, through its default export, so it loads in Node. 4.0.0 and earlier loaded only
36
+ under Bun: Node failed with "Named export 'verify' not found".
37
+ - **Bun:** loads with `import`, as before.
38
+ - **Peers:** `@occupop/lib-container` `^4.0.1`, the first 4.x version that re-exports awilix's
39
+ `asValue`, which this package imports; `typescript` `^5.6.3 || ^6.0.0`.
40
+
41
+ `bun run dist` ends with `bun run test:node`, which loads the built `dist/` with Node through both
42
+ `import()` and `require()`.
43
+
30
44
  ## Register
31
45
 
32
46
  ```typescript
@@ -118,6 +132,11 @@ await httpServer.listen(httpPort, () => {
118
132
  // ...
119
133
  ```
120
134
 
135
+ The middleware attaches the request-scoped container as `req.container`, then calls `next()`. If
136
+ building the context fails (for example, a request carries a token while `JWT_SECRET` is not set),
137
+ it calls `next(error)` instead, and Express's error handling takes over. Before 4.1.0 it never
138
+ called `next()`, so a request passing through it hung.
139
+
121
140
  ```typescript
122
141
  // app/router.ts
123
142
  export function makeAppRouter({ httpRouter }: Deps) {
@@ -144,5 +163,28 @@ export function makeGetJobsUseCase({ authUser, jobService }: Deps) {
144
163
  }
145
164
  ```
146
165
 
166
+ ## Signing a token for another service
167
+
168
+ `signAuthUserToken(authUser)` signs a user as a bearer token that another service verifies with
169
+ the same `JWT_SECRET`, for example to call the GraphQL gateway on that user's behalf:
147
170
 
171
+ ```typescript
172
+ import { signAuthUserToken } from '@occupop/lib-auth'
173
+
174
+ const token = signAuthUserToken(authUser) // a string, or null when authUser is null
175
+ const response = await fetch(gatewayUrl, {
176
+ method: 'POST',
177
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
178
+ body: JSON.stringify({ query }),
179
+ })
180
+ ```
148
181
 
182
+ - HS256 with `JWT_SECRET`, read when the function is called. If it is not set, the function throws
183
+ `'Env var JWT_SECRET not set?'`, as verification does.
184
+ - The payload is signed as given. A stored user that carries `iat` and `exp` keeps both; when it
185
+ has no `iat`, jsonwebtoken adds the signing time. No expiry, issuer or audience is added, so a
186
+ user without `exp` gives a token that does not expire.
187
+ - `null` in, `null` out.
188
+ - The token is identical to the one `@occupop/lib-context` 2.0.1's `makeGetAuthUserToken`
189
+ produces for the same user, secret and time. `src/index_test.ts` checks this against tokens
190
+ recorded from lib-context (`src/auth-user-token-vectors.json`).
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage } from 'node:http';
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { type AwilixContainer } from '@occupop/lib-container';
3
3
  type Request = IncomingMessage & {
4
4
  container?: AwilixContainer;
@@ -8,9 +8,24 @@ export type AuthUser = {
8
8
  readonly hiringCompanyUuid?: string;
9
9
  readonly recruitmentAgencyUuid?: string;
10
10
  readonly sysAdmin?: boolean;
11
+ readonly sysAdminEmail?: string;
11
12
  readonly isAdmin?: boolean;
12
13
  readonly isSuperAdmin?: boolean;
14
+ readonly permissions?: Permission;
13
15
  };
16
+ export interface Permission {
17
+ isSuperAdmin: boolean;
18
+ isAdmin: boolean;
19
+ candidates: boolean;
20
+ favorites: boolean;
21
+ recruiters: boolean;
22
+ formsDocuments: boolean;
23
+ contracts: boolean;
24
+ jobRequisition: boolean;
25
+ jobRequisitionCreate: boolean;
26
+ jobsCreate: boolean;
27
+ jobsPublish: boolean;
28
+ }
14
29
  export type ScopedUser = {
15
30
  readonly userUuid?: undefined;
16
31
  readonly [entityUuid: string]: {
@@ -19,6 +34,13 @@ export type ScopedUser = {
19
34
  };
20
35
  export declare function getAuthUser(req: Request): AuthUser | null;
21
36
  export declare function getScopedUser(req: Request): ScopedUser | null;
37
+ /**
38
+ * Signs `authUser` with `JWT_SECRET` (HS256), for calling another service on the user's behalf.
39
+ * The payload is signed as given: an `iat` or `exp` it carries is kept, jsonwebtoken adds `iat`
40
+ * only when it has none, and no expiry, issuer or audience is added. The token is the one
41
+ * `@occupop/lib-context` 2.0.1's `makeGetAuthUserToken` produces. Returns `null` for no user.
42
+ */
43
+ export declare function signAuthUserToken(authUser: AuthUser | null): string | null;
22
44
  export declare function makeAuthContextValueFunction<IContainer extends AwilixContainer>({ container }: {
23
45
  container: IContainer;
24
46
  }): ({ req }: {
@@ -26,7 +48,11 @@ export declare function makeAuthContextValueFunction<IContainer extends AwilixCo
26
48
  }) => Promise<{
27
49
  container: AwilixContainer<any>;
28
50
  }>;
51
+ /**
52
+ * Express middleware: attaches the request-scoped container as `req.container`, then calls
53
+ * `next()`. If building the context throws, it calls `next(error)` instead.
54
+ */
29
55
  export declare function makeAuthContextMiddleware<IContainer extends AwilixContainer>({ container, }: {
30
56
  container: IContainer;
31
- }): (req: Request) => Promise<void>;
57
+ }): (req: Request, res: ServerResponse, next: (error?: unknown) => void) => Promise<void>;
32
58
  export {};
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { asValue } from '@occupop/lib-container';
2
- import { verify } from 'jsonwebtoken';
2
+ import jwt from 'jsonwebtoken';
3
3
  function getToken(req) {
4
4
  return req.headers.authorization?.replace(/^Bearer /, '');
5
5
  }
@@ -9,7 +9,7 @@ function checkToken(token) {
9
9
  if (!process.env.JWT_SECRET)
10
10
  throw 'Env var JWT_SECRET not set?';
11
11
  try {
12
- return verify(token, process.env.JWT_SECRET);
12
+ return jwt.verify(token, process.env.JWT_SECRET);
13
13
  }
14
14
  catch (e) {
15
15
  return null;
@@ -31,6 +31,19 @@ export function getScopedUser(req) {
31
31
  return user;
32
32
  return null;
33
33
  }
34
+ /**
35
+ * Signs `authUser` with `JWT_SECRET` (HS256), for calling another service on the user's behalf.
36
+ * The payload is signed as given: an `iat` or `exp` it carries is kept, jsonwebtoken adds `iat`
37
+ * only when it has none, and no expiry, issuer or audience is added. The token is the one
38
+ * `@occupop/lib-context` 2.0.1's `makeGetAuthUserToken` produces. Returns `null` for no user.
39
+ */
40
+ export function signAuthUserToken(authUser) {
41
+ if (!authUser)
42
+ return null;
43
+ if (!process.env.JWT_SECRET)
44
+ throw 'Env var JWT_SECRET not set?';
45
+ return jwt.sign(authUser, process.env.JWT_SECRET);
46
+ }
34
47
  export function makeAuthContextValueFunction({ container }) {
35
48
  return async ({ req }) => {
36
49
  const authUser = getAuthUser(req);
@@ -43,10 +56,22 @@ export function makeAuthContextValueFunction({ container }) {
43
56
  return { container: scopedContainer };
44
57
  };
45
58
  }
59
+ /**
60
+ * Express middleware: attaches the request-scoped container as `req.container`, then calls
61
+ * `next()`. If building the context throws, it calls `next(error)` instead.
62
+ */
46
63
  export function makeAuthContextMiddleware({ container, }) {
47
- return async (req) => {
48
- const authContextValueFunction = makeAuthContextValueFunction({ container });
49
- const contextValue = await authContextValueFunction({ req });
50
- req.container = contextValue.container;
64
+ return async (req, res, next) => {
65
+ try {
66
+ const authContextValueFunction = makeAuthContextValueFunction({
67
+ container,
68
+ });
69
+ const contextValue = await authContextValueFunction({ req });
70
+ req.container = contextValue.container;
71
+ }
72
+ catch (error) {
73
+ return next(error);
74
+ }
75
+ next();
51
76
  };
52
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occupop/lib-auth",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "index.ts",
6
6
  "description": "...",
@@ -12,7 +12,8 @@
12
12
  "test": "bun test",
13
13
  "cover": "bun test --coverage",
14
14
  "lint": "biome lint",
15
- "dist": "bun lint && bun cover && rm -fr dist && tsc"
15
+ "test:node": "node test/node-load.mjs",
16
+ "dist": "bun lint && bun cover && rm -fr dist && tsc && bun run test:node"
16
17
  },
17
18
  "type": "module",
18
19
  "types": "dist/index.d.ts",
@@ -20,11 +21,12 @@
20
21
  "jsonwebtoken": "^9.0.2"
21
22
  },
22
23
  "peerDependencies": {
23
- "@occupop/lib-container": "^4.0.0",
24
- "typescript": "^5.6.3"
24
+ "@occupop/lib-container": "^4.0.1",
25
+ "typescript": "^5.6.3 || ^6.0.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@biomejs/biome": "^1.9.4",
29
+ "@occupop/lib-container": "^4.0.1",
28
30
  "@types/bun": "latest",
29
31
  "@types/jsonwebtoken": "^9.0.7"
30
32
  }