@harperfast/template-react-ts-studio 1.12.3 → 1.12.5
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 +362 -322
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +62 -3
- 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 +362 -322
- package/agent/skills/harper-best-practices/SKILL.md +9 -3
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/agent/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/agent/skills/harper-best-practices/rules.manifest.yaml +62 -3
- package/agent/skills/harper-mcp/AGENTS.md +77 -31
- package/agent/skills/harper-mcp/SKILL.md +10 -3
- 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
|
@@ -19,14 +19,14 @@ Use this skill when you need to define new data structures or modify existing on
|
|
|
19
19
|
2. **Use Directives**: All available directives for defining your schema are defined in `node_modules/harper/schema.graphql`. Common directives include `@table`, `@export`, `@primaryKey`, `@indexed`, and `@relationship`.
|
|
20
20
|
3. **Define Relationships**: Link tables together using the `@relationship` directive. For more details, see the [Defining Relationships](defining-relationships.md) skill.
|
|
21
21
|
4. **Enable Automatic APIs**: If you add `@table @export` to a schema type, Harper automatically sets up REST and WebSocket APIs for basic CRUD operations against that table. **Important**: REST endpoints also require `rest: true` in `config.yaml` — without it, `@export`ed tables will not respond to HTTP requests. For a detailed list of available endpoints and how to use them, see the [Automatic REST APIs](automatic-apis.md) skill.
|
|
22
|
-
- `GET /{TableName}`: Describes the
|
|
22
|
+
- `GET /{TableName}`: Describes the resource itself — table, database, and declared attributes. No trailing slash.
|
|
23
23
|
- `GET /{TableName}/`: Lists all records (supports filtering, sorting, and pagination via query parameters). See the [Querying REST APIs](querying-rest-apis.md) skill for details.
|
|
24
|
-
- `GET /{TableName}/{id}`: Retrieves a single record by its
|
|
25
|
-
- `POST /{TableName}/`: Creates a
|
|
26
|
-
- `PUT /{TableName}/{id}`:
|
|
27
|
-
- `PATCH /{TableName}/{id}`:
|
|
28
|
-
- `DELETE /{TableName}
|
|
29
|
-
- `DELETE /{TableName}
|
|
24
|
+
- `GET /{TableName}/{id}`: Retrieves a single record by its primary key.
|
|
25
|
+
- `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`.
|
|
26
|
+
- `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). **Properties omitted from the body are removed** — send the complete record, or use `PATCH` to change a subset. Three exceptions survive the replacement: an `@updatedTime` attribute is re-stamped with the time of the write, a `@createdTime` attribute keeps its original value, and the primary key is forced to match the `{id}` in the URL, so a mismatched key in the body cannot create a second record.
|
|
27
|
+
- `PATCH /{TableName}/{id}`: Merges the request body into the existing record, preserving unspecified properties. The merge is **shallow** — a nested object in the body replaces the stored one wholesale rather than being deep-merged.
|
|
28
|
+
- `DELETE /{TableName}/{id}`: Deletes a single record by its primary key.
|
|
29
|
+
- `DELETE /{TableName}/`: Deletes every record matching the query parameters. **With no query parameters it matches — and deletes — every record in the table.** Always pass a filter unless emptying the table is the intent.
|
|
30
30
|
5. **Consider Table Extensions**: If you are going to [extend the table](./extending-tables.md) in your resources, then do not `@export` the table from the schema.
|
|
31
31
|
|
|
32
32
|
## Examples
|
|
@@ -7,6 +7,7 @@ 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
12
|
- reference/v5/security/jwt-authentication.md#Create Authentication Tokens
|
|
12
13
|
- reference/v5/security/jwt-authentication.md#Using the Operation Token
|
|
@@ -18,8 +19,8 @@ metadata:
|
|
|
18
19
|
- reference/v5/security/jwt-authentication.md#Token Expiry Configuration
|
|
19
20
|
- reference/v5/security/jwt-authentication.md#When to Use JWT Auth
|
|
20
21
|
- reference/v5/security/jwt-authentication.md#Security Notes
|
|
21
|
-
sourceCommit:
|
|
22
|
-
inputHash:
|
|
22
|
+
sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
|
|
23
|
+
inputHash: 1837f39fdfe06fac
|
|
23
24
|
---
|
|
24
25
|
|
|
25
26
|
# Checking Authentication
|
|
@@ -28,207 +29,100 @@ Instructions for the agent to handle user authentication, sessions, and JWT toke
|
|
|
28
29
|
|
|
29
30
|
## When to Use
|
|
30
31
|
|
|
31
|
-
Apply this rule when implementing login/logout flows,
|
|
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.
|
|
32
33
|
|
|
33
34
|
## How It Works
|
|
34
35
|
|
|
35
|
-
1.
|
|
36
|
-
|
|
37
|
-
```javascript
|
|
38
|
-
async get(target) {
|
|
39
|
-
const user = this.getCurrentUser();
|
|
40
|
-
if (!user) return new Response(null, { status: 401 });
|
|
41
|
-
return { username: user.username, role: user.role };
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
2. **Enable sessions before using login/logout**: Set `authentication.enableSessions: true` in `harperdb-config.yaml`. Without this, `context.login` and `context.session` are unavailable.
|
|
46
|
-
|
|
47
|
-
3. **Implement login via `getContext()`**: Call `this.getContext()` to obtain the request context, then call `context.login(username, password)` to verify credentials and establish a session cookie.
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
export class SignIn extends Resource {
|
|
51
|
-
async post(_target, data) {
|
|
52
|
-
const context = this.getContext();
|
|
53
|
-
try {
|
|
54
|
-
await context.login(data.username, data.password);
|
|
55
|
-
} catch {
|
|
56
|
-
return new Response('Invalid credentials', { status: 403 });
|
|
57
|
-
}
|
|
58
|
-
return new Response('Logged in', { status: 200 });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
4. **Implement logout**: Delete the session via `context.session.delete(context.session.id)`.
|
|
64
|
-
|
|
65
|
-
```javascript
|
|
66
|
-
export class SignOut extends Resource {
|
|
67
|
-
async post() {
|
|
68
|
-
const context = this.getContext();
|
|
69
|
-
if (!context.session) return new Response(null, { status: 401 });
|
|
70
|
-
await context.session.delete(context.session.id);
|
|
71
|
-
return new Response('Logged out', { status: 200 });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Cookie-based sessions are intended for browser clients. For non-browser clients, use JWT issuance (steps below).
|
|
77
|
-
|
|
78
|
-
5. **Create authentication tokens**: Call `create_authentication_tokens` with credentials. No `Authorization` header is required for this operation.
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{
|
|
82
|
-
"operation": "create_authentication_tokens",
|
|
83
|
-
"username": "username",
|
|
84
|
-
"password": "password"
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Response:
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
{
|
|
92
|
-
"operation_token": "<jwt-operation-token>",
|
|
93
|
-
"refresh_token": "<jwt-refresh-token>"
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
6. **Use the operation token**: Pass it as a `Bearer` token in the `Authorization` header on subsequent requests.
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
curl --location --request POST 'http://localhost:9925' \
|
|
101
|
-
--header 'Content-Type: application/json' \
|
|
102
|
-
--header 'Authorization: Bearer <operation_token>' \
|
|
103
|
-
--data-raw '{
|
|
104
|
-
"operation": "search_by_hash",
|
|
105
|
-
"schema": "dev",
|
|
106
|
-
"table": "dog",
|
|
107
|
-
"hash_values": [1],
|
|
108
|
-
"get_attributes": ["*"]
|
|
109
|
-
}'
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
7. **Refresh an expired operation token**: When the `operation_token` expires, use `refresh_operation_token` and pass the `refresh_token` as `Bearer <refresh_token>`.
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
curl --location --request POST 'http://localhost:9925' \
|
|
116
|
-
--header 'Content-Type: application/json' \
|
|
117
|
-
--header 'Authorization: Bearer <refresh_token>' \
|
|
118
|
-
--data-raw '{
|
|
119
|
-
"operation": "refresh_operation_token"
|
|
120
|
-
}'
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Response:
|
|
124
|
-
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"operation_token": "<new-jwt-operation-token>"
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
When both tokens have expired, call `create_authentication_tokens` again with username and password.
|
|
132
|
-
|
|
133
|
-
8. **Mint scoped tokens for limited access**: A super user can embed an inline role in `create_authentication_tokens` using the same `permission` structure as `add_role`. Include `expires_in` to control lifetime. Do not include a `password` field. The `username` is attribution only and must not match an existing user.
|
|
134
|
-
|
|
135
|
-
```json
|
|
136
|
-
{
|
|
137
|
-
"operation": "create_authentication_tokens",
|
|
138
|
-
"username": "reporting-service",
|
|
139
|
-
"role": {
|
|
140
|
-
"permission": {
|
|
141
|
-
"operations": ["read_only"],
|
|
142
|
-
"dev": {
|
|
143
|
-
"tables": {
|
|
144
|
-
"dog": {
|
|
145
|
-
"read": true,
|
|
146
|
-
"insert": false,
|
|
147
|
-
"update": false,
|
|
148
|
-
"delete": false,
|
|
149
|
-
"attribute_permissions": []
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
"expires_in": "7d"
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Key constraints for scoped tokens:
|
|
160
|
-
- No refresh token is issued; no user record is created.
|
|
161
|
-
- Scoped tokens cannot be revoked before expiry — choose short `expires_in` values.
|
|
162
|
-
- `super_user` and `cluster_user` are always forced to `false` in the embedded role.
|
|
163
|
-
- In mixed-version clusters, only nodes with scoped-token support accept these tokens; older nodes return 401.
|
|
164
|
-
|
|
165
|
-
9. **Issue tokens from a custom Resource using `server.operation`**: Import `server` from `harper` and call `server.operation()` to mint tokens programmatically. Pass `true` as the **third argument** to run the operation as the current authenticated user; omit it when supplying credentials directly.
|
|
166
|
-
|
|
167
|
-
```javascript
|
|
168
|
-
import { Resource, server } from 'harper';
|
|
169
|
-
|
|
170
|
-
export class IssueTokens extends Resource {
|
|
171
|
-
static async get(_target, context) {
|
|
172
|
-
const { operation_token, refresh_token } = await server.operation(
|
|
173
|
-
{ operation: 'create_authentication_tokens' },
|
|
174
|
-
context,
|
|
175
|
-
true,
|
|
176
|
-
);
|
|
177
|
-
return { operation_token, refresh_token };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
static async post(_target, data) {
|
|
181
|
-
const { username, password } = await data;
|
|
182
|
-
if (!username || !password) {
|
|
183
|
-
return new Response('username and password required', { status: 400 });
|
|
184
|
-
}
|
|
185
|
-
const { operation_token, refresh_token } = await server.operation({
|
|
186
|
-
operation: 'create_authentication_tokens',
|
|
187
|
-
username,
|
|
188
|
-
password,
|
|
189
|
-
});
|
|
190
|
-
return { operation_token, refresh_token };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export class RefreshJWT extends Resource {
|
|
195
|
-
static async post(_target, data) {
|
|
196
|
-
const { refresh_token } = await data;
|
|
197
|
-
if (!refresh_token) {
|
|
198
|
-
return new Response('refresh_token required', { status: 400 });
|
|
199
|
-
}
|
|
200
|
-
const { operation_token } = await server.operation({
|
|
201
|
-
operation: 'refresh_operation_token',
|
|
202
|
-
refresh_token,
|
|
203
|
-
});
|
|
204
|
-
return { operation_token };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
10. **Configure token expiry**: Set timeouts in `harper-config.yaml` under the `authentication` section. Values follow the `jsonwebtoken` duration string format (e.g., `1d`, `12h`, `60m`).
|
|
210
|
-
|
|
211
|
-
```yaml
|
|
212
|
-
authentication:
|
|
213
|
-
operationTokenTimeout: 1d # Default: 1 day
|
|
214
|
-
refreshTokenTimeout: 30d # Default: 30 days
|
|
215
|
-
```
|
|
36
|
+
### 1. Reading the Current Authenticated User
|
|
216
37
|
|
|
217
|
-
|
|
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.
|
|
218
39
|
|
|
219
|
-
|
|
40
|
+
**In a static verb**, read the user from the `context` argument instead — `context.user`:
|
|
220
41
|
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
42
|
+
```javascript
|
|
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 };
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
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:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
export class SignIn extends Resource {
|
|
77
|
+
static async post(_target, data, context) {
|
|
78
|
+
const { username, password } = (await data) ?? {};
|
|
79
|
+
try {
|
|
80
|
+
await context.login(username, password);
|
|
81
|
+
} catch {
|
|
82
|
+
return new Response('Invalid credentials', { status: 403 });
|
|
83
|
+
}
|
|
84
|
+
return new Response('Logged in', { status: 200 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class SignOut extends Resource {
|
|
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 });
|
|
92
|
+
return new Response('Logged out', { status: 200 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
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:
|
|
230
105
|
|
|
231
|
-
|
|
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
|
|
232
126
|
curl --location --request POST 'http://localhost:9925' \
|
|
233
127
|
--header 'Content-Type: application/json' \
|
|
234
128
|
--header 'Authorization: Bearer <operation_token>' \
|
|
@@ -239,8 +133,13 @@ curl --location --request POST 'http://localhost:9925' \
|
|
|
239
133
|
"hash_values": [1],
|
|
240
134
|
"get_attributes": ["*"]
|
|
241
135
|
}'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 5. Refreshing the Operation Token
|
|
242
139
|
|
|
243
|
-
|
|
140
|
+
When the `operation_token` expires, pass the `refresh_token` as `Bearer <refresh_token>` and call `refresh_operation_token`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
244
143
|
curl --location --request POST 'http://localhost:9925' \
|
|
245
144
|
--header 'Content-Type: application/json' \
|
|
246
145
|
--header 'Authorization: Bearer <refresh_token>' \
|
|
@@ -249,14 +148,138 @@ curl --location --request POST 'http://localhost:9925' \
|
|
|
249
148
|
}'
|
|
250
149
|
```
|
|
251
150
|
|
|
252
|
-
|
|
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:
|
|
214
|
+
|
|
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
|
+
|
|
244
|
+
export class RefreshJWT extends Resource {
|
|
245
|
+
static async post(_target, data) {
|
|
246
|
+
const { refresh_token } = await data;
|
|
247
|
+
if (!refresh_token) {
|
|
248
|
+
return new Response('refresh_token required', { status: 400 });
|
|
249
|
+
}
|
|
250
|
+
const { operation_token } = await server.operation({
|
|
251
|
+
operation: 'refresh_operation_token',
|
|
252
|
+
refresh_token,
|
|
253
|
+
});
|
|
254
|
+
return { operation_token };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
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
|
|
253
276
|
|
|
254
277
|
```javascript
|
|
255
278
|
export class SignIn extends Resource {
|
|
256
|
-
async post(_target, data) {
|
|
257
|
-
const
|
|
279
|
+
static async post(_target, data, context) {
|
|
280
|
+
const { username, password } = (await data) ?? {};
|
|
258
281
|
try {
|
|
259
|
-
await context.login(
|
|
282
|
+
await context.login(username, password);
|
|
260
283
|
} catch {
|
|
261
284
|
return new Response('Invalid credentials', { status: 403 });
|
|
262
285
|
}
|
|
@@ -265,20 +288,41 @@ export class SignIn extends Resource {
|
|
|
265
288
|
}
|
|
266
289
|
|
|
267
290
|
export class SignOut extends Resource {
|
|
268
|
-
async post() {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await context.session.delete(context.session.id);
|
|
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 });
|
|
272
294
|
return new Response('Logged out', { status: 200 });
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
```
|
|
276
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
|
+
|
|
277
321
|
## Notes
|
|
278
322
|
|
|
279
|
-
- JWT authentication is **preferred over Basic Auth** when you want to avoid sending credentials on every request,
|
|
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**.
|
|
280
324
|
- Always use **HTTPS** in production to protect tokens in transit.
|
|
281
325
|
- Treat tokens like passwords. If a token is compromised, it remains valid until expiry — use shorter `operationTokenTimeout` values in high-security environments.
|
|
282
|
-
- `
|
|
283
|
-
- The `third argument
|
|
284
|
-
-
|
|
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.
|