@antelopejs/interface-auth 0.0.7 → 0.2.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.
@@ -1,10 +1,14 @@
1
- # Parameter Decoration
1
+ ---
2
+ title: Require authentication in controllers
3
+ description: Protect a handler, a property, or a whole controller with @Authentication, and build reusable decorators with CreateAuthDecorator.
4
+ mode: how-to
5
+ ---
2
6
 
3
- The Interface Auth provides decorators for integrating authentication into API controllers. These decorators extract, verify, and inject authentication data into handler parameters, class properties, and entire controller classes.
7
+ Use a parameter decorator when one handler needs the payload. Use a property decorator when several methods need it. Use a class decorator when every route must authenticate without receiving the payload. Pick one scope per need. You do not have to combine them.
4
8
 
5
9
  ## The `@Authentication` decorator
6
10
 
7
- The `@Authentication` decorator retrieves and validates a token from `x-antelopejs-auth`, or from the `ANTELOPEJS_AUTH` cookie when the header is absent, and injects the verified payload into the parameter.
11
+ The `@Authentication` decorator reads a token from `x-antelopejs-auth`, or from the `ANTELOPEJS_AUTH` cookie when the header is absent. It verifies the token and injects the verified payload into the parameter.
8
12
 
9
13
  ```typescript
10
14
  import { Controller, Get } from "@antelopejs/interface-api";
@@ -21,7 +25,9 @@ class UserController extends Controller("/users") {
21
25
  }
22
26
  ```
23
27
 
24
- If the token is missing or invalid, the request fails with an error response before the handler executes.
28
+ If the token is missing or invalid, the verifier rejects and the handler does not run. With `@antelopejs/auth-jwt`, the response is a 401 for a missing token and a 403 for an invalid one.
29
+
30
+ The payload annotations in these examples describe the token format the application expects. They do not check it. Add a [shape-checking verifier](./2.authentication.md#create-a-custom-verifier) before you read claims from tokens whose format nothing enforces yet. The API provider decides how an ordinary thrown error becomes an HTTP response. Throwing an `Error` does not set a 401 or 403 status.
25
31
 
26
32
  ### Inline validators
27
33
 
@@ -34,13 +40,15 @@ import { Authentication } from "@antelopejs/interface-auth";
34
40
  class PaymentController extends Controller("/payments") {
35
41
  @Post()
36
42
  async createPayment(
37
- @Authentication((user: any) => {
38
- if (!user.paymentPermissions) {
43
+ @Authentication((claims) => {
44
+ if (typeof claims === "string" || !claims.paymentPermissions) {
39
45
  throw new Error("Payment permissions required");
40
46
  }
41
- return user;
47
+ return claims;
42
48
  })
43
- user: any,
49
+ user: {
50
+ paymentPermissions: string[];
51
+ },
44
52
  ) {
45
53
  // Process payment
46
54
  return { success: true };
@@ -50,51 +58,59 @@ class PaymentController extends Controller("/payments") {
50
58
 
51
59
  ## Create custom decorators with `CreateAuthDecorator`
52
60
 
53
- The `CreateAuthDecorator` function creates a reusable authentication decorator with custom source, authenticator, and validator functions.
61
+ The `CreateAuthDecorator` function creates a reusable authentication decorator from custom source, authenticator, and validator functions. `validateApiKey` in the example below belongs to your application.
54
62
 
55
- ```typescript
63
+ ```typescript fragment
56
64
  import { CreateAuthDecorator } from "@antelopejs/interface-auth";
57
65
  import type { IncomingMessage } from "node:http";
58
66
 
59
67
  // Admin-only decorator
60
68
  export const AdminAuth = CreateAuthDecorator({
61
- validator: (user: any) => {
62
- if (!user || user.role !== "admin") {
69
+ validator: (claims) => {
70
+ if (typeof claims === "string" || claims.role !== "admin") {
63
71
  throw new Error("Admin access required");
64
72
  }
65
- return user;
73
+ return claims;
66
74
  },
67
75
  });
68
76
 
69
77
  // API key decorator with custom source and authenticator
70
78
  export const ApiKeyAuth = CreateAuthDecorator({
71
- source: (req: IncomingMessage) => req.headers["x-api-key"] as string,
79
+ source: (req: IncomingMessage) => {
80
+ const key = req.headers["x-api-key"];
81
+ return typeof key === "string" ? key : undefined;
82
+ },
72
83
  authenticator: async (apiKey?: string) => {
73
- if (!apiKey) return null;
84
+ if (!apiKey) throw new Error("API key required");
74
85
  const user = await validateApiKey(apiKey);
86
+ if (!user) throw new Error("Invalid API key");
75
87
  return user;
76
88
  },
77
89
  });
78
90
  ```
79
91
 
92
+ `validateApiKey` is your application's lookup and verification function. If the authenticator returned `null` for a missing or invalid key, the handler would run with `null`. This example throws instead.
93
+
80
94
  ### `CreateAuthDecorator` options
81
95
 
82
96
  The function accepts an object with the following properties:
83
97
 
84
- | Property | Type | Description |
85
- | ---------------------- | --------------------- | --------------------------------------------------------------- |
86
- | `source` | `AuthSource` | Extracts the token from the request (defaults to header/cookie) |
87
- | `authenticator` | `AuthVerifier<T>` | Verifies the token and returns the payload |
88
- | `authenticatorOptions` | `VerifyOptions` | Options passed to the authenticator |
89
- | `validator` | `AuthValidator<T, R>` | Validates and transforms the authenticated data |
98
+ | Property | Type | Description |
99
+ | ---------------------- | --------------------- | --------------------------------------------------------------------- |
100
+ | `source` | `AuthSource` | Extracts the token from the request. Defaults to header, then cookie. |
101
+ | `authenticator` | `AuthVerifier<T>` | Verifies the token and returns the payload |
102
+ | `authenticatorOptions` | `VerifyOptions` | Options passed to the authenticator |
103
+ | `validator` | `AuthValidator<T, R>` | Validates and transforms the authenticated data |
104
+
105
+ All properties are optional. The default source reads the header, then the cookie. The default authenticator calls `ValidateRaw`, and the default verification options are `{}`. Without a validator, the decorator injects the verified payload unchanged.
90
106
 
91
- All properties are optional. The default source reads the header/cookie, the default authenticator calls `ValidateRaw`, and the default verification options are `{}`. Without a validator, the verified payload is passed through unchanged.
107
+ TypeScript infers `T` and `R` from the callbacks. Without an `authenticator`, `T` is `AuthClaims`, which is `string | JsonObject`. A validator then checks the claims before it reads them, as in `AdminAuth` above. With an `authenticator`, `T` is what the authenticator returns, and the validator receives that type. To name both type arguments, write `CreateAuthDecorator<T, R>` with an `authenticator`. A single type argument names `R` when there is no `authenticator`, and `T` when there is one.
92
108
 
93
- A validator passed to a decorator call replaces the configured validator; the two do not compose. Return a transformed value to inject it, or throw to deny access. Returning `false` or `null` does not reject the request. A custom authenticator that accepts a missing token also permits it unless a validator throws.
109
+ A validator passed to a decorator call replaces the configured validator. The two do not compose. Return a transformed value to inject it, or throw to deny access. Returning `false` or `null` does not reject the request. A custom authenticator that accepts a missing token lets the request through unless a validator throws.
94
110
 
95
111
  ## Class-level authentication
96
112
 
97
- The `@Authentication` decorator can be applied to an entire controller class. This ensures that all routes within that controller require authentication, without decorating each handler individually.
113
+ Apply the `@Authentication` decorator to a controller class to require authentication on every route of that controller. You do not need to decorate each handler.
98
114
 
99
115
  ```typescript
100
116
  import { Controller, Get, Post } from "@antelopejs/interface-api";
@@ -124,11 +140,11 @@ Combine class-level authentication with a validator to enforce role-based access
124
140
  import { Controller } from "@antelopejs/interface-api";
125
141
  import { Authentication } from "@antelopejs/interface-auth";
126
142
 
127
- @Authentication((user: any) => {
128
- if (!user || user.role !== "admin") {
143
+ @Authentication((claims) => {
144
+ if (typeof claims === "string" || claims.role !== "admin") {
129
145
  throw new Error("Admin access required");
130
146
  }
131
- return user;
147
+ return claims;
132
148
  })
133
149
  class AdminController extends Controller("/admin") {
134
150
  // All routes require admin authentication
@@ -137,7 +153,7 @@ class AdminController extends Controller("/admin") {
137
153
 
138
154
  ## Property-level authentication
139
155
 
140
- Apply `@Authentication` to a class property to access the authenticated user data throughout the controller. The property is populated automatically for each request.
156
+ Apply `@Authentication` to a class property to read the authenticated user data in every handler of the controller. The API module sets the property on each request.
141
157
 
142
158
  ```typescript
143
159
  import { Controller, Get } from "@antelopejs/interface-api";
@@ -165,7 +181,7 @@ class UserController extends Controller("/users") {
165
181
 
166
182
  ## Decorator scope summary
167
183
 
168
- The `@Authentication` decorator (and any custom decorator created with `CreateAuthDecorator`) can be applied at three levels:
184
+ The `@Authentication` decorator, and any custom decorator that `CreateAuthDecorator` creates, applies at three levels:
169
185
 
170
186
  | Scope | Effect |
171
187
  | --------- | --------------------------------------------------------------------- |
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 480" role="img" aria-labelledby="title desc">
2
+ <title id="title">Authentication: extraction, trust, policy</title>
3
+ <desc id="desc">A source extracts a token. An authenticator verifies it. An optional validator checks policy. Only successful processing injects the result. Throwing stops injection.</desc>
4
+ <rect width="480" height="480" rx="16" fill="#f5f7fb"/>
5
+ <g font-family="system-ui, sans-serif" fill="#172b45">
6
+ <text x="24" y="36" font-size="20" font-weight="700">From request to trusted input</text>
7
+ <rect x="24" y="58" width="432" height="84" rx="10" fill="#e1ecfa"/>
8
+ <text x="42" y="87" font-size="18" font-weight="700">1 · Source</text>
9
+ <text x="42" y="116" font-size="16">Extract the token; do not trust it yet.</text>
10
+ <path d="M240 148v20m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
11
+ <rect x="24" y="176" width="432" height="84" rx="10" fill="#d9eee8"/>
12
+ <text x="42" y="205" font-size="18" font-weight="700">2 · Authenticator</text>
13
+ <text x="42" y="234" font-size="16">Verify the token and return its payload.</text>
14
+ <path d="M240 266v20m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
15
+ <rect x="24" y="294" width="432" height="84" rx="10" fill="#eee6fb"/>
16
+ <text x="42" y="323" font-size="18" font-weight="700">3 · Validator (optional)</text>
17
+ <text x="42" y="352" font-size="16">Check policy; return the value to inject.</text>
18
+ <text x="24" y="416" font-size="16" font-weight="700">Success → inject the resulting value.</text>
19
+ <text x="24" y="447" font-size="16" fill="#913a31">Throw → stop. Returning false does not deny.</text>
20
+ </g>
21
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-auth",
3
- "version": "0.0.7",
3
+ "version": "0.2.0",
4
4
  "description": "AntelopeJS interface for token authentication: sign and verify tokens and inject the verified payload into API handlers",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -46,13 +46,22 @@
46
46
  "release": "pnpm run lint && pnpm run prepack && release-it",
47
47
  "test": "pnpm run build && ajs module test .",
48
48
  "format:check": "oxfmt --check .",
49
- "knip": "knip"
49
+ "knip": "knip",
50
+ "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
51
+ "checks": "antelopejs-checks",
52
+ "size": "antelopejs-check-size",
53
+ "sync": "antelopejs-sync",
54
+ "sync:check": "antelopejs-sync --check",
55
+ "typecheck": "tsc --noEmit",
56
+ "suppressions": "antelopejs-suppressions",
57
+ "suppressions:update": "antelopejs-suppressions --update"
50
58
  },
51
59
  "devDependencies": {
60
+ "@antelopejs/auth-jwt": "1.0.3",
52
61
  "@antelopejs/core": "^1.16.0",
53
- "@antelopejs/interface-api": "^0.0.15",
54
- "@antelopejs/interface-core": "^0.2.4",
55
- "@antelopejs/tooling-configs": "^0.0.9",
62
+ "@antelopejs/interface-api": "^0.2.0",
63
+ "@antelopejs/interface-core": "^0.4.0",
64
+ "@antelopejs/tooling-configs": "^0.2.17",
56
65
  "@types/chai": "^4.3.20",
57
66
  "@types/mocha": "^10.0.10",
58
67
  "@types/node": "^22.19.15",
@@ -60,6 +69,7 @@
60
69
  "chai": "^4.5.0",
61
70
  "eslint-plugin-perfectionist": "^5.11.0",
62
71
  "knip": "^6.34.0",
72
+ "lefthook": "^2.2.2",
63
73
  "mongodb-memory-server-core": "^10.4.3",
64
74
  "oxfmt": "^0.66.0",
65
75
  "oxlint": "1.81.0",
@@ -74,10 +84,19 @@
74
84
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0"
75
85
  },
76
86
  "packageManager": "pnpm@10.6.5",
87
+ "pnpm": {
88
+ "overrides": {
89
+ "@antelopejs/auth-jwt>@antelopejs/interface-auth": "link:.",
90
+ "@antelopejs/interface-core": "0.4.0"
91
+ }
92
+ },
77
93
  "antelopeJs": {
78
94
  "test": "src/antelope.test.ts",
79
95
  "skills": [
80
96
  "./skills"
81
- ]
97
+ ],
98
+ "docs": {
99
+ "standard": true
100
+ }
82
101
  }
83
102
  }
@@ -24,9 +24,12 @@ import {
24
24
  ValidateRaw,
25
25
  SignServerResponse,
26
26
  internal, // provider side only
27
+ type AuthClaims,
27
28
  type AuthSource,
28
29
  type AuthVerifier,
29
30
  type AuthValidator,
31
+ type JsonObject,
32
+ type JsonValue,
30
33
  type SignOptions,
31
34
  type VerifyOptions,
32
35
  type CookieOptions,
@@ -61,7 +64,9 @@ class LibrarianController extends Controller("/librarians") {
61
64
  }
62
65
  ```
63
66
 
64
- Custom pipelines use `CreateAuthDecorator({ source?, authenticator?, authenticatorOptions?, validator? })`; the callbacks run as `source(req, res)` → `authenticator(data, authenticatorOptions)` → `validator(data)` → injected parameter.
67
+ Custom pipelines use `CreateAuthDecorator({ source?, authenticator?, authenticatorOptions?, validator? })`; the callbacks run as `source(req, res)` → `authenticator(data, authenticatorOptions)` → `validator(data)` → injected parameter. Without an `authenticator` the validator receives an `AuthClaims`; with one it receives what the authenticator returns.
68
+
69
+ `ValidateRaw` has no type parameter: it returns `AuthClaims` (`string | JsonObject`). To read fields, parse the claims once, in a custom `authenticator` (`async (token, options) => parseSession(await ValidateRaw(token, options))`) or at the call site.
65
70
 
66
71
  ## Providing
67
72
 
@@ -72,7 +77,7 @@ import { ImplementInterface } from "@antelopejs/interface-core";
72
77
  import { internal } from "@antelopejs/interface-auth";
73
78
 
74
79
  ImplementInterface(internal, {
75
- Verify: (token, options) => decodeAndVerify(token, options), // return payload, throw on invalid
80
+ Verify: (token, options) => decodeAndVerify(token, options), // return the AuthClaims (string or JSON object), throw on invalid
76
81
  Sign: (data, options) => signToken(data, options), // return token string
77
82
  });
78
83
  ```
@@ -81,6 +86,7 @@ Declare `"antelopeJs": { "implements": ["@antelopejs/interface-auth"] }` in the
81
86
 
82
87
  ## Gotchas
83
88
 
89
+ - The data passed to `SignRaw` / `SignServerResponse` is a `string`, a `Buffer` or a `JsonObject`. A `Date`, a class instance or an `interface`-typed object (no index signature) is a compile error; convert it or use a `type` alias. `undefined`-valued keys are accepted.
84
90
  - `SignRaw` / `ValidateRaw` / `SignServerResponse` are interface proxy calls: they return Promises and only resolve once a provider module is attached. While a provider module is loaded but not yet attached, earlier calls are queued, not failed — always `await`. But when no loaded module provides the interface (e.g. a stubbed `optionalDependencies` entry), the core neutralizes the proxies and those calls reject instead of queuing.
85
91
  - Default token source (`internal.defaultSource`): the `x-antelopejs-auth` request header, falling back to the `ANTELOPEJS_AUTH` cookie. `SignServerResponse` sets that same cookie via `Set-Cookie`.
86
92
  - `@Authentication(validator?)` accepts an optional validator at the use site; it overrides any validator configured in `CreateAuthDecorator`.
@@ -0,0 +1,73 @@
1
+ {
2
+ "version": 1,
3
+ "cases": [
4
+ {
5
+ "id": "protect-route",
6
+ "area": "framework",
7
+ "style": "clean",
8
+ "prompt": "How do I protect a route with authentication?",
9
+ "origin": "docs-assistant",
10
+ "expect": {
11
+ "skills": ["auth-interface"],
12
+ "acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
13
+ "mustMention": ["Authentication"]
14
+ }
15
+ },
16
+ {
17
+ "id": "protect-route-casual",
18
+ "area": "framework",
19
+ "style": "casual",
20
+ "prompt": "what do i need to do so my route is protected?",
21
+ "origin": "docs-assistant",
22
+ "expect": {
23
+ "skills": ["auth-interface"],
24
+ "acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
25
+ "mustMention": ["Authentication"]
26
+ }
27
+ },
28
+ {
29
+ "id": "auth-interface-dms-neighbour",
30
+ "area": "dms",
31
+ "style": "clean",
32
+ "prompt": "In the DMS, only administrators should open the billing page. Register a permission for it and gate the page.",
33
+ "expect": {
34
+ "skills": ["dms-auth"],
35
+ "acceptable": ["dms", "dms-pages"],
36
+ "avoid": ["auth-interface"]
37
+ }
38
+ },
39
+ {
40
+ "id": "auth-interface-cookie-response",
41
+ "area": "framework",
42
+ "style": "clean",
43
+ "prompt": "After login I want the signed token set as a cookie on the response instead of returned in the body. What do I call?",
44
+ "expect": {
45
+ "skills": ["auth-interface"],
46
+ "acceptable": ["api-interface", "ajs-consume-interface", "antelopejs"],
47
+ "mustMention": ["SignServerResponse"]
48
+ }
49
+ },
50
+ {
51
+ "id": "auth-interface-token-expiry-casual",
52
+ "area": "framework",
53
+ "style": "casual",
54
+ "prompt": "token dies too quick, how do i make it last a day",
55
+ "expect": {
56
+ "skills": ["auth-interface"],
57
+ "acceptable": ["antelopejs", "ajs-consume-interface"],
58
+ "mustMention": ["SignRaw", "expiresIn"]
59
+ }
60
+ },
61
+ {
62
+ "id": "auth-interface-typed-claims",
63
+ "area": "framework",
64
+ "style": "clean",
65
+ "prompt": "ValidateRaw<Profile>(token) no longer compiles. How do I get a typed profile out of a verified token?",
66
+ "expect": {
67
+ "skills": ["auth-interface"],
68
+ "acceptable": ["antelopejs", "ajs-consume-interface"],
69
+ "mustMention": ["AuthClaims"]
70
+ }
71
+ }
72
+ ]
73
+ }