@stonyx/orm 0.3.2-alpha.55 → 0.3.2-alpha.57
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/dist/index.d.ts +1 -1
- package/dist/main.d.ts +16 -1
- package/dist/main.js +18 -1
- package/dist/types/orm-types.d.ts +44 -10
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/main.ts +18 -1
- package/src/orm-request.ts +2 -2
- package/src/types/orm-types.ts +45 -10
- package/src/types/stonyx-rest-server.d.ts +14 -1
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { count, avg, sum, min, max } from './aggregates.js';
|
|
|
9
9
|
export { default } from './main.js';
|
|
10
10
|
export { store, relationships } from './main.js';
|
|
11
11
|
export type { PersistErrorDetail } from './main.js';
|
|
12
|
-
export type { AccessContext, AccessFunction, AccessMethod } from './types/orm-types.js';
|
|
12
|
+
export type { AccessContext, AccessFunction, AccessMethod, AccessOperation } from './types/orm-types.js';
|
|
13
13
|
export { Model, View, Serializer };
|
|
14
14
|
export { attr, belongsTo, hasMany, createRecord, updateRecord };
|
|
15
15
|
export { count, avg, sum, min, max };
|
package/dist/main.d.ts
CHANGED
|
@@ -81,8 +81,23 @@ export default class Orm {
|
|
|
81
81
|
* collection the request is ADDRESSED TO -- owners -- while being asked about
|
|
82
82
|
* animals, and per #202's thesis it answers wrong in the granting direction.
|
|
83
83
|
*
|
|
84
|
+
* OWN PROPERTIES ONLY. A bare `this.accessFiles[modelName]` walks the
|
|
85
|
+
* prototype chain, so `getAccess('constructor')` resolved `Object` and
|
|
86
|
+
* `getAccess('toString')` resolved `Object.prototype.toString` -- both
|
|
87
|
+
* callable, and the documented `predicate?.(request, ctx)` pattern then
|
|
88
|
+
* returned a TRUTHY value (`Object(request)` is the request), bypassing the
|
|
89
|
+
* `undefined`-means-deny contract entirely. Nothing in the ORM calls
|
|
90
|
+
* `getAccess` yet, so it was not exploitable as shipped -- but #207 takes the
|
|
91
|
+
* model name from the REQUEST BODY (`data.relationships.<key>.data.type`),
|
|
92
|
+
* which would have made a one-field body an authorization bypass. Guarded
|
|
93
|
+
* here at the read point rather than by constructing the map with a null
|
|
94
|
+
* prototype, because the field is public and reassignable and the guard has
|
|
95
|
+
* to hold whatever object it is holding.
|
|
96
|
+
*
|
|
84
97
|
* @param modelName - Model name as declared and stored (kebab-case).
|
|
85
|
-
* @returns The predicate, or `undefined` when
|
|
98
|
+
* @returns The predicate, or `undefined` when no predicate could be resolved
|
|
99
|
+
* for that name. `undefined` is NOT "this model is unrestricted" -- see the
|
|
100
|
+
* note above. Treat it as deny.
|
|
86
101
|
*/
|
|
87
102
|
getAccess(modelName: string): AccessFunction | undefined;
|
|
88
103
|
startup(): Promise<void>;
|
package/dist/main.js
CHANGED
|
@@ -187,10 +187,27 @@ export default class Orm {
|
|
|
187
187
|
* collection the request is ADDRESSED TO -- owners -- while being asked about
|
|
188
188
|
* animals, and per #202's thesis it answers wrong in the granting direction.
|
|
189
189
|
*
|
|
190
|
+
* OWN PROPERTIES ONLY. A bare `this.accessFiles[modelName]` walks the
|
|
191
|
+
* prototype chain, so `getAccess('constructor')` resolved `Object` and
|
|
192
|
+
* `getAccess('toString')` resolved `Object.prototype.toString` -- both
|
|
193
|
+
* callable, and the documented `predicate?.(request, ctx)` pattern then
|
|
194
|
+
* returned a TRUTHY value (`Object(request)` is the request), bypassing the
|
|
195
|
+
* `undefined`-means-deny contract entirely. Nothing in the ORM calls
|
|
196
|
+
* `getAccess` yet, so it was not exploitable as shipped -- but #207 takes the
|
|
197
|
+
* model name from the REQUEST BODY (`data.relationships.<key>.data.type`),
|
|
198
|
+
* which would have made a one-field body an authorization bypass. Guarded
|
|
199
|
+
* here at the read point rather than by constructing the map with a null
|
|
200
|
+
* prototype, because the field is public and reassignable and the guard has
|
|
201
|
+
* to hold whatever object it is holding.
|
|
202
|
+
*
|
|
190
203
|
* @param modelName - Model name as declared and stored (kebab-case).
|
|
191
|
-
* @returns The predicate, or `undefined` when
|
|
204
|
+
* @returns The predicate, or `undefined` when no predicate could be resolved
|
|
205
|
+
* for that name. `undefined` is NOT "this model is unrestricted" -- see the
|
|
206
|
+
* note above. Treat it as deny.
|
|
192
207
|
*/
|
|
193
208
|
getAccess(modelName) {
|
|
209
|
+
if (!Object.hasOwn(this.accessFiles, modelName))
|
|
210
|
+
return undefined;
|
|
194
211
|
return this.accessFiles[modelName];
|
|
195
212
|
}
|
|
196
213
|
async startup() {
|
|
@@ -176,6 +176,20 @@ export interface SnapshotEntry {
|
|
|
176
176
|
* Anything else fails CLOSED. See `src/orm-request.ts` `auth()`.
|
|
177
177
|
*/
|
|
178
178
|
export type AccessMethod = string | boolean | string[] | ((record: unknown) => boolean);
|
|
179
|
+
/**
|
|
180
|
+
* The closed vocabulary `AccessContext.operation` is drawn from
|
|
181
|
+
* (abofs/stonyx-orm#202).
|
|
182
|
+
*
|
|
183
|
+
* A literal union rather than `string`, so the guarantee the prose makes is the
|
|
184
|
+
* one the compiler enforces: a consumer who writes `operation === 'GET'` or
|
|
185
|
+
* `operation === 'get'` -- the hook vocabulary, see below -- gets a compile
|
|
186
|
+
* error instead of a comparison that never matches. A predicate that stops
|
|
187
|
+
* matching falls through to the permission array, so the misreading is
|
|
188
|
+
* fail-open shaped.
|
|
189
|
+
*
|
|
190
|
+
* In-repo precedent: `PersistErrorDetail.operation` in `src/main.ts`.
|
|
191
|
+
*/
|
|
192
|
+
export type AccessOperation = 'read' | 'create' | 'update' | 'delete';
|
|
179
193
|
/**
|
|
180
194
|
* The structural facts about the request being authorised, handed to a consumer
|
|
181
195
|
* `access()` predicate as its SECOND argument (abofs/stonyx-orm#202).
|
|
@@ -205,27 +219,47 @@ export interface AccessContext {
|
|
|
205
219
|
*/
|
|
206
220
|
model: string;
|
|
207
221
|
/**
|
|
208
|
-
* The operation being authorised
|
|
209
|
-
*
|
|
222
|
+
* The operation being authorised. Exactly one of the four {@link
|
|
223
|
+
* AccessOperation} verbs, or `undefined`. These are exactly the values of
|
|
210
224
|
* `methodAccessMap` in `src/orm-request.ts`, which is also what the
|
|
211
225
|
* permission-array return shape is matched against -- so the two forms cannot
|
|
212
226
|
* disagree.
|
|
213
227
|
*
|
|
228
|
+
* NOT the hook vocabulary. `HookContext.operation` (`src/hooks.ts`) carries
|
|
229
|
+
* `'list' | 'get' | 'create' | 'update' | 'delete'` on an identically-named
|
|
230
|
+
* key of an identically-shaped context object, and the access vocabulary
|
|
231
|
+
* collapses `list` and `get` into `'read'`. For one `GET /animals/1` a hook
|
|
232
|
+
* sees `'get'` and `access()` sees `'read'`. "No second vocabulary" is a
|
|
233
|
+
* statement about the ACCESS path only.
|
|
234
|
+
*
|
|
214
235
|
* `undefined` when the dispatched method has no entry in that map. Express
|
|
215
236
|
* delivers `HEAD` to the `GET` handler, so this is reachable. It is left
|
|
216
237
|
* undefined rather than defaulted on purpose: a fabricated `'read'` would
|
|
217
238
|
* turn an unclassified request into an authorised one.
|
|
239
|
+
*
|
|
240
|
+
* The KEY is required even though the value may be undefined: `auth()` always
|
|
241
|
+
* sets it, and a context that simply omitted it would be indistinguishable
|
|
242
|
+
* from one that classified the request and found nothing.
|
|
218
243
|
*/
|
|
219
|
-
operation
|
|
244
|
+
operation: AccessOperation | undefined;
|
|
220
245
|
}
|
|
221
246
|
/**
|
|
222
247
|
* A consumer `access()` predicate.
|
|
223
248
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
249
|
+
* The second argument is ADDITIVE: JavaScript ignores extra arguments, so every
|
|
250
|
+
* pre-#202 single-argument predicate keeps working untouched. Changing the
|
|
251
|
+
* FIRST argument instead would have been the breaking form, and a predicate
|
|
252
|
+
* that can no longer identify its collection falls through to a full CRUD
|
|
253
|
+
* grant -- so the "safer" breaking change would have converted every unmigrated
|
|
254
|
+
* predicate into a fail-open.
|
|
255
|
+
*
|
|
256
|
+
* `context` is nonetheless REQUIRED in the type, and that costs back-compat
|
|
257
|
+
* nothing. TypeScript already lets a fewer-parameter implementation satisfy a
|
|
258
|
+
* more-parameter signature, so an arity-1 predicate assigns to this type
|
|
259
|
+
* cleanly -- measured under `--strict`. What the `?` bought was the opposite of
|
|
260
|
+
* safety: it silently permitted `getAccess('animal')?.(request)` at the CALL
|
|
261
|
+
* site, i.e. exactly the omission {@link AccessContext} exists to prevent, and
|
|
262
|
+
* that call gets the model-wrong answer. Required, a caller that drops the
|
|
263
|
+
* context gets `TS2554: Expected 2 arguments, but got 1`.
|
|
230
264
|
*/
|
|
231
|
-
export type AccessFunction = (request: unknown, context
|
|
265
|
+
export type AccessFunction = (request: unknown, context: AccessContext) => AccessMethod;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { count, avg, sum, min, max } from './aggregates.js';
|
|
|
27
27
|
export { default } from './main.js';
|
|
28
28
|
export { store, relationships } from './main.js';
|
|
29
29
|
export type { PersistErrorDetail } from './main.js';
|
|
30
|
-
export type { AccessContext, AccessFunction, AccessMethod } from './types/orm-types.js'; // access() contract (#202)
|
|
30
|
+
export type { AccessContext, AccessFunction, AccessMethod, AccessOperation } from './types/orm-types.js'; // access() contract (#202)
|
|
31
31
|
export { Model, View, Serializer }; // base classes
|
|
32
32
|
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
33
33
|
export { count, avg, sum, min, max }; // aggregate helpers
|
package/src/main.ts
CHANGED
|
@@ -240,10 +240,27 @@ export default class Orm {
|
|
|
240
240
|
* collection the request is ADDRESSED TO -- owners -- while being asked about
|
|
241
241
|
* animals, and per #202's thesis it answers wrong in the granting direction.
|
|
242
242
|
*
|
|
243
|
+
* OWN PROPERTIES ONLY. A bare `this.accessFiles[modelName]` walks the
|
|
244
|
+
* prototype chain, so `getAccess('constructor')` resolved `Object` and
|
|
245
|
+
* `getAccess('toString')` resolved `Object.prototype.toString` -- both
|
|
246
|
+
* callable, and the documented `predicate?.(request, ctx)` pattern then
|
|
247
|
+
* returned a TRUTHY value (`Object(request)` is the request), bypassing the
|
|
248
|
+
* `undefined`-means-deny contract entirely. Nothing in the ORM calls
|
|
249
|
+
* `getAccess` yet, so it was not exploitable as shipped -- but #207 takes the
|
|
250
|
+
* model name from the REQUEST BODY (`data.relationships.<key>.data.type`),
|
|
251
|
+
* which would have made a one-field body an authorization bypass. Guarded
|
|
252
|
+
* here at the read point rather than by constructing the map with a null
|
|
253
|
+
* prototype, because the field is public and reassignable and the guard has
|
|
254
|
+
* to hold whatever object it is holding.
|
|
255
|
+
*
|
|
243
256
|
* @param modelName - Model name as declared and stored (kebab-case).
|
|
244
|
-
* @returns The predicate, or `undefined` when
|
|
257
|
+
* @returns The predicate, or `undefined` when no predicate could be resolved
|
|
258
|
+
* for that name. `undefined` is NOT "this model is unrestricted" -- see the
|
|
259
|
+
* note above. Treat it as deny.
|
|
245
260
|
*/
|
|
246
261
|
getAccess(modelName: string): AccessFunction | undefined {
|
|
262
|
+
if (!Object.hasOwn(this.accessFiles, modelName)) return undefined;
|
|
263
|
+
|
|
247
264
|
return this.accessFiles[modelName];
|
|
248
265
|
}
|
|
249
266
|
|
package/src/orm-request.ts
CHANGED
|
@@ -122,7 +122,7 @@ import { getBeforeHooks, getAfterHooks } from './hooks.js';
|
|
|
122
122
|
import type { HookContext } from './hooks.js';
|
|
123
123
|
import config from 'stonyx/config';
|
|
124
124
|
import log from 'stonyx/log';
|
|
125
|
-
import type { OrmRecord, AccessContext, AccessFunction, AccessMethod } from './types/orm-types.js';
|
|
125
|
+
import type { OrmRecord, AccessContext, AccessFunction, AccessMethod, AccessOperation } from './types/orm-types.js';
|
|
126
126
|
import { isOrmRecord } from './utils.js';
|
|
127
127
|
|
|
128
128
|
interface OrmRequest$ extends Request {
|
|
@@ -152,7 +152,7 @@ interface JsonApiResponse {
|
|
|
152
152
|
|
|
153
153
|
type HandlerFn = (request: OrmRequest$, state: { [key: string]: unknown }) => unknown | Promise<unknown>;
|
|
154
154
|
|
|
155
|
-
const methodAccessMap: { [key: string]:
|
|
155
|
+
const methodAccessMap: { [key: string]: AccessOperation } = {
|
|
156
156
|
GET: 'read',
|
|
157
157
|
POST: 'create',
|
|
158
158
|
DELETE: 'delete',
|
package/src/types/orm-types.ts
CHANGED
|
@@ -184,6 +184,21 @@ export interface SnapshotEntry {
|
|
|
184
184
|
*/
|
|
185
185
|
export type AccessMethod = string | boolean | string[] | ((record: unknown) => boolean);
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The closed vocabulary `AccessContext.operation` is drawn from
|
|
189
|
+
* (abofs/stonyx-orm#202).
|
|
190
|
+
*
|
|
191
|
+
* A literal union rather than `string`, so the guarantee the prose makes is the
|
|
192
|
+
* one the compiler enforces: a consumer who writes `operation === 'GET'` or
|
|
193
|
+
* `operation === 'get'` -- the hook vocabulary, see below -- gets a compile
|
|
194
|
+
* error instead of a comparison that never matches. A predicate that stops
|
|
195
|
+
* matching falls through to the permission array, so the misreading is
|
|
196
|
+
* fail-open shaped.
|
|
197
|
+
*
|
|
198
|
+
* In-repo precedent: `PersistErrorDetail.operation` in `src/main.ts`.
|
|
199
|
+
*/
|
|
200
|
+
export type AccessOperation = 'read' | 'create' | 'update' | 'delete';
|
|
201
|
+
|
|
187
202
|
/**
|
|
188
203
|
* The structural facts about the request being authorised, handed to a consumer
|
|
189
204
|
* `access()` predicate as its SECOND argument (abofs/stonyx-orm#202).
|
|
@@ -214,28 +229,48 @@ export interface AccessContext {
|
|
|
214
229
|
model: string;
|
|
215
230
|
|
|
216
231
|
/**
|
|
217
|
-
* The operation being authorised
|
|
218
|
-
*
|
|
232
|
+
* The operation being authorised. Exactly one of the four {@link
|
|
233
|
+
* AccessOperation} verbs, or `undefined`. These are exactly the values of
|
|
219
234
|
* `methodAccessMap` in `src/orm-request.ts`, which is also what the
|
|
220
235
|
* permission-array return shape is matched against -- so the two forms cannot
|
|
221
236
|
* disagree.
|
|
222
237
|
*
|
|
238
|
+
* NOT the hook vocabulary. `HookContext.operation` (`src/hooks.ts`) carries
|
|
239
|
+
* `'list' | 'get' | 'create' | 'update' | 'delete'` on an identically-named
|
|
240
|
+
* key of an identically-shaped context object, and the access vocabulary
|
|
241
|
+
* collapses `list` and `get` into `'read'`. For one `GET /animals/1` a hook
|
|
242
|
+
* sees `'get'` and `access()` sees `'read'`. "No second vocabulary" is a
|
|
243
|
+
* statement about the ACCESS path only.
|
|
244
|
+
*
|
|
223
245
|
* `undefined` when the dispatched method has no entry in that map. Express
|
|
224
246
|
* delivers `HEAD` to the `GET` handler, so this is reachable. It is left
|
|
225
247
|
* undefined rather than defaulted on purpose: a fabricated `'read'` would
|
|
226
248
|
* turn an unclassified request into an authorised one.
|
|
249
|
+
*
|
|
250
|
+
* The KEY is required even though the value may be undefined: `auth()` always
|
|
251
|
+
* sets it, and a context that simply omitted it would be indistinguishable
|
|
252
|
+
* from one that classified the request and found nothing.
|
|
227
253
|
*/
|
|
228
|
-
operation
|
|
254
|
+
operation: AccessOperation | undefined;
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
/**
|
|
232
258
|
* A consumer `access()` predicate.
|
|
233
259
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
260
|
+
* The second argument is ADDITIVE: JavaScript ignores extra arguments, so every
|
|
261
|
+
* pre-#202 single-argument predicate keeps working untouched. Changing the
|
|
262
|
+
* FIRST argument instead would have been the breaking form, and a predicate
|
|
263
|
+
* that can no longer identify its collection falls through to a full CRUD
|
|
264
|
+
* grant -- so the "safer" breaking change would have converted every unmigrated
|
|
265
|
+
* predicate into a fail-open.
|
|
266
|
+
*
|
|
267
|
+
* `context` is nonetheless REQUIRED in the type, and that costs back-compat
|
|
268
|
+
* nothing. TypeScript already lets a fewer-parameter implementation satisfy a
|
|
269
|
+
* more-parameter signature, so an arity-1 predicate assigns to this type
|
|
270
|
+
* cleanly -- measured under `--strict`. What the `?` bought was the opposite of
|
|
271
|
+
* safety: it silently permitted `getAccess('animal')?.(request)` at the CALL
|
|
272
|
+
* site, i.e. exactly the omission {@link AccessContext} exists to prevent, and
|
|
273
|
+
* that call gets the model-wrong answer. Required, a caller that drops the
|
|
274
|
+
* context gets `TS2554: Expected 2 arguments, but got 1`.
|
|
240
275
|
*/
|
|
241
|
-
export type AccessFunction = (request: unknown, context
|
|
276
|
+
export type AccessFunction = (request: unknown, context: AccessContext) => AccessMethod;
|
|
@@ -5,7 +5,20 @@ declare module '@stonyx/rest-server' {
|
|
|
5
5
|
|
|
6
6
|
interface RouteOptions {
|
|
7
7
|
name: string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `access` is the two-argument post-#202 shape. This is the THIRD place the
|
|
10
|
+
* contract is declared (`AccessInstance.access` in
|
|
11
|
+
* `src/setup-rest-server.ts` and `OrmRequest.access` in
|
|
12
|
+
* `src/orm-request.ts` are the other two) and it is the one `mountRoute` is
|
|
13
|
+
* actually called through, at `src/setup-rest-server.ts`. It kept the
|
|
14
|
+
* pre-#202 single-argument signature after the other two migrated; the
|
|
15
|
+
* union with `Record<string, unknown>` meant nothing broke, which is
|
|
16
|
+
* exactly why it would have drifted silently.
|
|
17
|
+
*
|
|
18
|
+
* Spelled structurally rather than as `AccessFunction`: an ambient
|
|
19
|
+
* `declare module` block cannot carry an `import type`.
|
|
20
|
+
*/
|
|
21
|
+
options?: { model: string; access: (request: unknown, context: { model: string; operation: string | undefined }) => unknown } | Record<string, unknown>;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
export default class RestServer {
|