@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.
@@ -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 schema itself.
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 ID.
25
- - `POST /{TableName}/`: Creates a new record.
26
- - `PUT /{TableName}/{id}`: Updates an existing record.
27
- - `PATCH /{TableName}/{id}`: Performs a partial update on a record.
28
- - `DELETE /{TableName}/`: Deletes all records or filtered records.
29
- - `DELETE /{TableName}/{id}`: Deletes a single record by its ID.
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: 677ad213d67822e109c83619e181ca23a59823db
22
- inputHash: 084363f039abfe73
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, protecting Resource endpoints by checking the current user, issuing or refreshing JWT tokens, or configuring token expiry in Harper. Use it whenever a custom Resource needs to authenticate callers or mint credentials for downstream consumers. See [custom-resources.md](custom-resources.md) for the broader Resource authoring context.
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. **Check the current authenticated user**: Call `getCurrentUser()` inside any Resource method. It returns the user object (with `username`, `role`, and `role.permission`) or `undefined` if unauthenticated. Guard endpoints by returning a 401 when no user is present.
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
- ## Examples
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
- ### Full JWT flow via cURL
40
+ **In a static verb**, read the user from the `context` argument instead — `context.user`:
220
41
 
221
- ```bash
222
- # Step 1: Create tokens
223
- curl --location --request POST 'http://localhost:9925' \
224
- --header 'Content-Type: application/json' \
225
- --data-raw '{
226
- "operation": "create_authentication_tokens",
227
- "username": "username",
228
- "password": "password"
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
- # Step 2: Use operation token
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
- # Step 3: Refresh when operation token expires
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
- ### Session-based login/logout Resource
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 context = this.getContext();
279
+ static async post(_target, data, context) {
280
+ const { username, password } = (await data) ?? {};
258
281
  try {
259
- await context.login(data.username, data.password);
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
- const context = this.getContext();
270
- if (!context.session) return new Response(null, { status: 401 });
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, when the client can store tokens, or when making multiple sequential requests. For simple or server-to-server scenarios, use Basic Authentication.
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
- - `enableSessions` must be `true` in config before `context.login` or `context.session` will work.
283
- - The `third argument` (`true`) to `server.operation` controls whether the operation runs as the current authenticated user. Omit it when the operation body supplies its own credentials.
284
- - Scoped tokens have a 12KB limit when encoded into an `Authorization` header.
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.