@harperfast/template-react-ts-studio 1.12.2 → 1.12.4
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/.agents/skills/harper-best-practices/AGENTS.md +778 -580
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +131 -55
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +265 -127
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +63 -85
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +125 -52
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +38 -70
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +81 -69
- package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +82 -96
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +273 -2
- package/.agents/skills/harper-mcp/AGENTS.md +77 -31
- package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/.agents/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/agent/skills/harper-best-practices/AGENTS.md +778 -580
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +131 -55
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +265 -127
- package/agent/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +63 -85
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +125 -52
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +38 -70
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +81 -69
- package/agent/skills/harper-best-practices/rules/v5-upgrade.md +82 -96
- package/agent/skills/harper-best-practices/rules.manifest.yaml +273 -2
- package/agent/skills/harper-mcp/AGENTS.md +77 -31
- package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/agent/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/package.json +1 -1
- package/skills-lock.json +2 -2
|
@@ -7,146 +7,77 @@ metadata:
|
|
|
7
7
|
- >-
|
|
8
8
|
reference/v5/resources/resource-api.md#`getCurrentUser(): User |
|
|
9
9
|
undefined`
|
|
10
|
+
- 'reference/v5/resources/resource-api.md#`getContext(): Context`'
|
|
10
11
|
- reference/v5/resources/resource-api.md#Session and Login from a Resource
|
|
11
|
-
- reference/v5/security/jwt-authentication.md
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
- reference/v5/security/jwt-authentication.md#Create Authentication Tokens
|
|
13
|
+
- reference/v5/security/jwt-authentication.md#Using the Operation Token
|
|
14
|
+
- reference/v5/security/jwt-authentication.md#Refreshing the Operation Token
|
|
15
|
+
- reference/v5/security/jwt-authentication.md#Scoped Tokens (Inline Role)
|
|
16
|
+
- >-
|
|
17
|
+
reference/v5/security/jwt-authentication.md#Issuing Tokens from a Custom
|
|
18
|
+
Resource
|
|
19
|
+
- reference/v5/security/jwt-authentication.md#Token Expiry Configuration
|
|
20
|
+
- reference/v5/security/jwt-authentication.md#When to Use JWT Auth
|
|
21
|
+
- reference/v5/security/jwt-authentication.md#Security Notes
|
|
22
|
+
sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
|
|
23
|
+
inputHash: 1837f39fdfe06fac
|
|
14
24
|
---
|
|
15
25
|
|
|
16
26
|
# Checking Authentication
|
|
17
27
|
|
|
18
|
-
Instructions for the agent to
|
|
28
|
+
Instructions for the agent to handle user authentication, sessions, and JWT token issuance in Harper Resources.
|
|
19
29
|
|
|
20
30
|
## When to Use
|
|
21
31
|
|
|
22
|
-
Apply this rule when implementing
|
|
32
|
+
Apply this rule when implementing login/logout flows, reading the current authenticated user, issuing or refreshing JWT tokens, or minting scoped tokens from a custom Resource. Use it whenever a Resource must gate behavior on identity or credentials. See [custom-resources.md](custom-resources.md) for the broader Resource authoring model.
|
|
23
33
|
|
|
24
34
|
## How It Works
|
|
25
35
|
|
|
26
|
-
1.
|
|
27
|
-
|
|
28
|
-
```javascript
|
|
29
|
-
async get(target) {
|
|
30
|
-
const user = this.getCurrentUser();
|
|
31
|
-
if (!user) return new Response(null, { status: 401 });
|
|
32
|
-
return { username: user.username, role: user.role };
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The returned object exposes `username`, `role`, and `role.permission` flags.
|
|
37
|
-
|
|
38
|
-
2. **Enable sessions** before using session-based login. Set `authentication.enableSessions: true` in `harperdb-config.yaml`:
|
|
39
|
-
|
|
40
|
-
```yaml
|
|
41
|
-
authentication:
|
|
42
|
-
enableSessions: true
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
3. **Access login and session helpers** via `getContext()`. The context object exposes `context.login` and `context.session` for sign-in/out flows.
|
|
46
|
-
- Call `context.login(username, password)` to verify credentials and establish a session cookie on success.
|
|
47
|
-
- To end a session, delete it via `context.session.delete(context.session.id)`.
|
|
48
|
-
|
|
49
|
-
4. **Implement sign-in and sign-out Resources** using the context helpers:
|
|
50
|
-
|
|
51
|
-
```javascript
|
|
52
|
-
export class SignIn extends Resource {
|
|
53
|
-
async post(_target, data) {
|
|
54
|
-
const context = this.getContext();
|
|
55
|
-
try {
|
|
56
|
-
await context.login(data.username, data.password);
|
|
57
|
-
} catch {
|
|
58
|
-
return new Response('Invalid credentials', { status: 403 });
|
|
59
|
-
}
|
|
60
|
-
return new Response('Logged in', { status: 200 });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class SignOut extends Resource {
|
|
65
|
-
async post() {
|
|
66
|
-
const context = this.getContext();
|
|
67
|
-
if (!context.session) return new Response(null, { status: 401 });
|
|
68
|
-
await context.session.delete(context.session.id);
|
|
69
|
-
return new Response('Logged out', { status: 200 });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
5. **Issue JWTs for non-browser clients** (CLI tools, mobile apps, service-to-service). Cookie-based sessions are intended for browser clients. For other clients, mint tokens programmatically using `server.operation()`:
|
|
75
|
-
|
|
76
|
-
```javascript
|
|
77
|
-
import { Resource, server } from 'harper';
|
|
78
|
-
|
|
79
|
-
export class IssueTokens extends Resource {
|
|
80
|
-
static async get(_target, context) {
|
|
81
|
-
const { operation_token, refresh_token } = await server.operation(
|
|
82
|
-
{ operation: 'create_authentication_tokens' },
|
|
83
|
-
context,
|
|
84
|
-
true,
|
|
85
|
-
);
|
|
86
|
-
return { operation_token, refresh_token };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
static async post(_target, data) {
|
|
90
|
-
const { username, password } = await data;
|
|
91
|
-
if (!username || !password) {
|
|
92
|
-
return new Response('username and password required', { status: 400 });
|
|
93
|
-
}
|
|
94
|
-
const { operation_token, refresh_token } = await server.operation({
|
|
95
|
-
operation: 'create_authentication_tokens',
|
|
96
|
-
username,
|
|
97
|
-
password,
|
|
98
|
-
});
|
|
99
|
-
return { operation_token, refresh_token };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export class RefreshJWT extends Resource {
|
|
104
|
-
static async post(_target, data) {
|
|
105
|
-
const { refresh_token } = await data;
|
|
106
|
-
if (!refresh_token) {
|
|
107
|
-
return new Response('refresh_token required', { status: 400 });
|
|
108
|
-
}
|
|
109
|
-
const { operation_token } = await server.operation({
|
|
110
|
-
operation: 'refresh_operation_token',
|
|
111
|
-
refresh_token,
|
|
112
|
-
});
|
|
113
|
-
return { operation_token };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Pass `true` as the third argument to `server.operation()` when the operation should run as the current authenticated user. Omit it or pass `false` when the operation supplies its own credentials.
|
|
119
|
-
|
|
120
|
-
6. **Configure JWT token expiry** in `harperdb-config.yaml` under the `authentication` section:
|
|
121
|
-
|
|
122
|
-
```yaml
|
|
123
|
-
authentication:
|
|
124
|
-
operationTokenTimeout: 1d
|
|
125
|
-
refreshTokenTimeout: 30d
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Duration strings follow the `jsonwebtoken` package format (e.g., `1d`, `12h`, `60m`).
|
|
36
|
+
### 1. Reading the Current Authenticated User
|
|
129
37
|
|
|
130
|
-
|
|
38
|
+
**In an instance method**, call `getCurrentUser()` to get the user associated with the current request, or `undefined` if unauthenticated. The returned object exposes `username`, `role`, and `role.permission` flags.
|
|
131
39
|
|
|
132
|
-
**
|
|
40
|
+
**In a static verb**, read the user from the `context` argument instead — `context.user`:
|
|
133
41
|
|
|
134
42
|
```javascript
|
|
135
|
-
async get(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
43
|
+
static async get(_target, context) {
|
|
44
|
+
const user = context?.user;
|
|
45
|
+
if (!user) return new Response(null, { status: 401 });
|
|
46
|
+
return { username: user.username, role: user.role };
|
|
139
47
|
}
|
|
140
48
|
```
|
|
141
49
|
|
|
142
|
-
|
|
50
|
+
### 2. Reading the Full Context
|
|
51
|
+
|
|
52
|
+
**In an instance method**, call `getContext()` to retrieve the current context, which includes:
|
|
53
|
+
|
|
54
|
+
- `user` — User object with username, role, and authorization information
|
|
55
|
+
- `transaction` — The current transaction
|
|
56
|
+
|
|
57
|
+
**In a static verb**, the context is the trailing argument:
|
|
58
|
+
|
|
59
|
+
- `(target, context)` for `get`/`delete`
|
|
60
|
+
- `(target, data, context)` for `put`/`patch`/`post`
|
|
61
|
+
|
|
62
|
+
When triggered by HTTP, the context also exposes `url`, `method`, `headers`, `responseHeaders`, `pathname`, `host`, `ip`, `body`, `data`, `lastModified`, and `requestContext`.
|
|
63
|
+
|
|
64
|
+
### 3. Handling Sessions and Login
|
|
65
|
+
|
|
66
|
+
Enable sessions in `harper-config.yaml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
authentication:
|
|
70
|
+
enableSessions: true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `context.login` in a static `post` verb to verify credentials and establish a session cookie:
|
|
143
74
|
|
|
144
75
|
```javascript
|
|
145
76
|
export class SignIn extends Resource {
|
|
146
|
-
async post(_target, data) {
|
|
147
|
-
const
|
|
77
|
+
static async post(_target, data, context) {
|
|
78
|
+
const { username, password } = (await data) ?? {};
|
|
148
79
|
try {
|
|
149
|
-
await context.login(
|
|
80
|
+
await context.login(username, password);
|
|
150
81
|
} catch {
|
|
151
82
|
return new Response('Invalid credentials', { status: 403 });
|
|
152
83
|
}
|
|
@@ -155,18 +86,161 @@ export class SignIn extends Resource {
|
|
|
155
86
|
}
|
|
156
87
|
|
|
157
88
|
export class SignOut extends Resource {
|
|
158
|
-
async post() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await context.session.delete(context.session.id);
|
|
89
|
+
static async post(_target, _data, context) {
|
|
90
|
+
if (!context?.session?.user) return new Response(null, { status: 401 });
|
|
91
|
+
await context.session.update({ user: null });
|
|
162
92
|
return new Response('Logged out', { status: 200 });
|
|
163
93
|
}
|
|
164
94
|
}
|
|
165
95
|
```
|
|
166
96
|
|
|
167
|
-
|
|
97
|
+
- `context.login(username, password)` verifies credentials and sets the session cookie on success.
|
|
98
|
+
- To end a session, call `context.session.update({ user: null })` — `update` is the session's only mutator.
|
|
99
|
+
- `context.session` is an empty object (not `undefined`) when sessions are enabled and no session cookie is present; test `context?.session?.user` to detect an established session.
|
|
100
|
+
- Cookie-based sessions are for browser clients. For non-browser clients, use JWT issuance.
|
|
101
|
+
|
|
102
|
+
### 4. Creating Authentication Tokens
|
|
103
|
+
|
|
104
|
+
Call `create_authentication_tokens` with credentials in the body — no `Authorization` header is required when `username` and `password` are supplied:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"operation": "create_authentication_tokens",
|
|
109
|
+
"username": "username",
|
|
110
|
+
"password": "password"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Response:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"operation_token": "<jwt-operation-token>",
|
|
119
|
+
"refresh_token": "<jwt-refresh-token>"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Pass the `operation_token` as a `Bearer` token on subsequent requests:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
127
|
+
--header 'Content-Type: application/json' \
|
|
128
|
+
--header 'Authorization: Bearer <operation_token>' \
|
|
129
|
+
--data-raw '{
|
|
130
|
+
"operation": "search_by_hash",
|
|
131
|
+
"schema": "dev",
|
|
132
|
+
"table": "dog",
|
|
133
|
+
"hash_values": [1],
|
|
134
|
+
"get_attributes": ["*"]
|
|
135
|
+
}'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 5. Refreshing the Operation Token
|
|
139
|
+
|
|
140
|
+
When the `operation_token` expires, pass the `refresh_token` as `Bearer <refresh_token>` and call `refresh_operation_token`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
144
|
+
--header 'Content-Type: application/json' \
|
|
145
|
+
--header 'Authorization: Bearer <refresh_token>' \
|
|
146
|
+
--data-raw '{
|
|
147
|
+
"operation": "refresh_operation_token"
|
|
148
|
+
}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Response:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"operation_token": "<new-jwt-operation-token>"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
When both tokens have expired, call `create_authentication_tokens` again with username and password.
|
|
160
|
+
|
|
161
|
+
### 6. Minting Scoped Tokens
|
|
162
|
+
|
|
163
|
+
A `super_user` can mint a scoped token with embedded permissions using `create_authentication_tokens` with an inline `role`. The minter must be authenticated; no `password` may be included in the body. Use `add_role`-style `permission` structure:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"operation": "create_authentication_tokens",
|
|
168
|
+
"username": "reporting-service",
|
|
169
|
+
"role": {
|
|
170
|
+
"permission": {
|
|
171
|
+
"operations": ["read_only"],
|
|
172
|
+
"dev": {
|
|
173
|
+
"tables": {
|
|
174
|
+
"dog": {
|
|
175
|
+
"read": true,
|
|
176
|
+
"insert": false,
|
|
177
|
+
"update": false,
|
|
178
|
+
"delete": false,
|
|
179
|
+
"attribute_permissions": []
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"expires_in": "7d"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Authenticate the mint request with Basic Authentication or an existing `super_user` `operation_token`:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
193
|
+
--header 'Content-Type: application/json' \
|
|
194
|
+
--header 'Authorization: Basic <base64 of super_user:password>' \
|
|
195
|
+
--data-raw '{
|
|
196
|
+
"operation": "create_authentication_tokens",
|
|
197
|
+
"username": "reporting-service",
|
|
198
|
+
"role": { "permission": { "operations": ["read_only"] } },
|
|
199
|
+
"expires_in": "7d"
|
|
200
|
+
}'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Key constraints for scoped tokens:
|
|
204
|
+
|
|
205
|
+
- `username` is attribution only and must not name an existing user.
|
|
206
|
+
- No refresh token is issued; no user record is created.
|
|
207
|
+
- Scoped tokens cannot be revoked before expiry — choose `expires_in` carefully and prefer short lifetimes.
|
|
208
|
+
- `super_user` and `cluster_user` are always forced to `false` in the embedded role.
|
|
209
|
+
- In **mixed-version** clusters, only nodes with scoped-token support accept these tokens; older nodes reject them with 401.
|
|
210
|
+
|
|
211
|
+
### 7. Issuing Tokens from a Custom Resource
|
|
212
|
+
|
|
213
|
+
Use `server.operation()` to mint tokens programmatically inside a Resource. Pass `authorize: true` as the **third argument** when the operation should run as the current authenticated user:
|
|
168
214
|
|
|
169
215
|
```javascript
|
|
216
|
+
import { Resource, server } from 'harper';
|
|
217
|
+
|
|
218
|
+
export class IssueTokens extends Resource {
|
|
219
|
+
static async get(_target, context) {
|
|
220
|
+
// Issue tokens for the current authenticated user
|
|
221
|
+
const { operation_token, refresh_token } = await server.operation(
|
|
222
|
+
{ operation: 'create_authentication_tokens' },
|
|
223
|
+
context,
|
|
224
|
+
true, // third argument: authorize as current user
|
|
225
|
+
);
|
|
226
|
+
return { operation_token, refresh_token };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static async post(_target, data) {
|
|
230
|
+
// Issue tokens from credentials supplied in the body
|
|
231
|
+
const { username, password } = await data;
|
|
232
|
+
if (!username || !password) {
|
|
233
|
+
return new Response('username and password required', { status: 400 });
|
|
234
|
+
}
|
|
235
|
+
const { operation_token, refresh_token } = await server.operation({
|
|
236
|
+
operation: 'create_authentication_tokens',
|
|
237
|
+
username,
|
|
238
|
+
password,
|
|
239
|
+
});
|
|
240
|
+
return { operation_token, refresh_token };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
170
244
|
export class RefreshJWT extends Resource {
|
|
171
245
|
static async post(_target, data) {
|
|
172
246
|
const { refresh_token } = await data;
|
|
@@ -182,9 +256,73 @@ export class RefreshJWT extends Resource {
|
|
|
182
256
|
}
|
|
183
257
|
```
|
|
184
258
|
|
|
259
|
+
Omit the third argument (or pass `false`) when the operation supplies its own credentials.
|
|
260
|
+
|
|
261
|
+
### 8. Configuring Token Expiry
|
|
262
|
+
|
|
263
|
+
Set timeouts in `harper-config.yaml` under the `authentication` section:
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
authentication:
|
|
267
|
+
operationTokenTimeout: 1d # Default: 1 day
|
|
268
|
+
refreshTokenTimeout: 30d # Default: 30 days
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Valid duration strings follow the `jsonwebtoken` package format (e.g., `1d`, `12h`, `60m`). The `expires_in` field on scoped token minting accepts the same format.
|
|
272
|
+
|
|
273
|
+
## Examples
|
|
274
|
+
|
|
275
|
+
### Full Sign-In / Sign-Out Resource
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
export class SignIn extends Resource {
|
|
279
|
+
static async post(_target, data, context) {
|
|
280
|
+
const { username, password } = (await data) ?? {};
|
|
281
|
+
try {
|
|
282
|
+
await context.login(username, password);
|
|
283
|
+
} catch {
|
|
284
|
+
return new Response('Invalid credentials', { status: 403 });
|
|
285
|
+
}
|
|
286
|
+
return new Response('Logged in', { status: 200 });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export class SignOut extends Resource {
|
|
291
|
+
static async post(_target, _data, context) {
|
|
292
|
+
if (!context?.session?.user) return new Response(null, { status: 401 });
|
|
293
|
+
await context.session.update({ user: null });
|
|
294
|
+
return new Response('Logged out', { status: 200 });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Reading the Current Authenticated User in a Static Verb
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
static async get(_target, context) {
|
|
303
|
+
const user = context?.user;
|
|
304
|
+
if (!user) return new Response(null, { status: 401 });
|
|
305
|
+
return { username: user.username, role: user.role };
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Minting Tokens via cURL
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
313
|
+
--header 'Content-Type: application/json' \
|
|
314
|
+
--data-raw '{
|
|
315
|
+
"operation": "create_authentication_tokens",
|
|
316
|
+
"username": "username",
|
|
317
|
+
"password": "password"
|
|
318
|
+
}'
|
|
319
|
+
```
|
|
320
|
+
|
|
185
321
|
## Notes
|
|
186
322
|
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
323
|
+
- JWT authentication is **preferred over Basic Auth** when you want to avoid sending credentials on every request, your client can store tokens, or you have multiple sequential requests. For simple or **server-to-server** scenarios, use **Basic Authentication**.
|
|
324
|
+
- Always use **HTTPS** in production to protect tokens in transit.
|
|
325
|
+
- Treat tokens like passwords. If a token is compromised, it remains valid until expiry — use shorter `operationTokenTimeout` values in high-security environments.
|
|
326
|
+
- `context.login` and `context.session` require `enableSessions: true` in `harperdb-config.yaml`; they are for browser clients only.
|
|
327
|
+
- The `server.operation()` third argument (`authorize: true`) attributes the operation to — and permission-checks against — the current authenticated user. Omit it when supplying credentials directly in the operation body.
|
|
328
|
+
- In **mixed-version** clusters, scoped tokens are only accepted by nodes that support them; older nodes return 401.
|
|
@@ -6,9 +6,9 @@ metadata:
|
|
|
6
6
|
sources:
|
|
7
7
|
- reference/v5/resources/overview.md#Custom External Data Source
|
|
8
8
|
- reference/v5/resources/overview.md#Exporting Resources as Endpoints
|
|
9
|
-
- reference/v5/components/javascript-environment.md#Module
|
|
10
|
-
sourceCommit:
|
|
11
|
-
inputHash:
|
|
9
|
+
- reference/v5/components/javascript-environment.md#Module Formats
|
|
10
|
+
sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
|
|
11
|
+
inputHash: 86858206e33925f0
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Custom Resources
|
|
@@ -17,11 +17,11 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
|
|
|
17
17
|
|
|
18
18
|
## When to Use
|
|
19
19
|
|
|
20
|
-
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed
|
|
20
|
+
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed resource or when a specific URL shape is required.
|
|
21
21
|
|
|
22
22
|
## How It Works
|
|
23
23
|
|
|
24
|
-
1. **Import `Resource` from `harper
|
|
24
|
+
1. **Import `Resource` from the `harper` package**: Always import explicitly rather than relying on globals.
|
|
25
25
|
|
|
26
26
|
```javascript
|
|
27
27
|
import { tables, Resource } from 'harper';
|
|
@@ -39,7 +39,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
3. **Use `async` static methods
|
|
42
|
+
3. **Use `async` static methods to call external services**: Return or forward the response directly.
|
|
43
43
|
|
|
44
44
|
```javascript
|
|
45
45
|
export class MyExternalData extends Resource {
|
|
@@ -57,7 +57,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
4. **Export the class to
|
|
60
|
+
4. **Export the class to expose it as an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
|
|
61
61
|
|
|
62
62
|
| Export form | URL | Notes |
|
|
63
63
|
| ------------------------------------------- | --------------- | --------------------------------------------------------------- |
|
|
@@ -68,12 +68,17 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
68
68
|
| `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
|
|
69
69
|
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
```javascript
|
|
74
|
+
server.resources.set('my-path', Foo);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
6. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
|
|
74
78
|
|
|
75
79
|
```javascript
|
|
76
80
|
export class Widget extends Resource {
|
|
81
|
+
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
77
82
|
static path = '/widget/:id/action/:action';
|
|
78
83
|
static get(target) {
|
|
79
84
|
return { id: target.id, action: target.action };
|
|
@@ -81,22 +86,11 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
81
86
|
}
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
|
|
87
|
-
|
|
88
|
-
```javascript
|
|
89
|
-
server.resources.set('my-path', Foo);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
|
|
93
|
-
```javascript
|
|
94
|
-
tables.MyCache.sourcedFrom(MyExternalData);
|
|
95
|
-
```
|
|
89
|
+
7. **Resolve path precedence correctly**: Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats a `:param`, which beats a `*` wildcard, compared left to right.
|
|
96
90
|
|
|
97
91
|
## Examples
|
|
98
92
|
|
|
99
|
-
|
|
93
|
+
**Wrapping an external API and using it as a cache source:**
|
|
100
94
|
|
|
101
95
|
```javascript
|
|
102
96
|
import { tables, Resource } from 'harper';
|
|
@@ -119,19 +113,9 @@ export class MyExternalData extends Resource {
|
|
|
119
113
|
tables.MyCache.sourcedFrom(MyExternalData);
|
|
120
114
|
```
|
|
121
115
|
|
|
122
|
-
|
|
116
|
+
**Catch-all wildcard path:**
|
|
123
117
|
|
|
124
118
|
```javascript
|
|
125
|
-
import { Resource } from 'harper';
|
|
126
|
-
|
|
127
|
-
export class Widget extends Resource {
|
|
128
|
-
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
129
|
-
static path = '/widget/:id/action/:action';
|
|
130
|
-
static get(target) {
|
|
131
|
-
return { id: target.id, action: target.action };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
119
|
export class Files extends Resource {
|
|
136
120
|
// GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
|
|
137
121
|
static path = '/files/*rest';
|
|
@@ -141,25 +125,24 @@ export class Files extends Resource {
|
|
|
141
125
|
}
|
|
142
126
|
```
|
|
143
127
|
|
|
144
|
-
|
|
128
|
+
**Root-relative fixed route:**
|
|
145
129
|
|
|
146
130
|
```javascript
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
export class Foo extends Resource {
|
|
131
|
+
export class AcmeChallenge extends Resource {
|
|
132
|
+
static path = '/.well-known/acme-challenge/:token';
|
|
150
133
|
static get(target) {
|
|
151
|
-
return {
|
|
134
|
+
return { token: target.token };
|
|
152
135
|
}
|
|
153
136
|
}
|
|
154
|
-
|
|
155
|
-
server.resources.set('my-path', Foo);
|
|
156
137
|
```
|
|
157
138
|
|
|
158
139
|
## Notes
|
|
159
140
|
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
141
|
+
- URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
|
|
142
|
+
- A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
|
|
143
|
+
- A leading `./` or a bare name in `static path` resolves relative to the component directory.
|
|
144
|
+
- A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
|
|
145
|
+
- `static path` takes precedence over the export name when both are present.
|
|
146
|
+
- Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as URI templates.
|
|
147
|
+
- When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
|
|
164
148
|
- Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
|
|
165
|
-
- Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
|