@stonyx/orm 0.3.2-beta.155 → 0.3.2-beta.157
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/README.md +243 -71
- package/dist/hooks.d.ts +15 -1
- package/dist/orm-request.d.ts +106 -23
- package/dist/orm-request.js +149 -23
- package/dist/types/orm-types.d.ts +95 -0
- package/package.json +1 -1
- package/src/hooks.ts +15 -1
- package/src/orm-request.ts +149 -23
- package/src/types/orm-types.ts +96 -0
package/src/types/orm-types.ts
CHANGED
|
@@ -252,6 +252,102 @@ export interface AccessContext {
|
|
|
252
252
|
* from one that classified the request and found nothing.
|
|
253
253
|
*/
|
|
254
254
|
operation: AccessOperation | undefined;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The record this route was addressed to, as the store key -- or `null` on a
|
|
258
|
+
* collection route, which is addressed to no record (abofs/stonyx-orm#236).
|
|
259
|
+
*
|
|
260
|
+
* IT IS ALREADY DECODED, AND THAT IS THE WHOLE POINT. Express decodes route
|
|
261
|
+
* PARAMETERS while leaving `request.path` raw, so a consumer comparing
|
|
262
|
+
* `request.path` against a literal compares an undecoded string against a
|
|
263
|
+
* decoded dispatch. `GET /owners/%61rchived` reached such a comparison as
|
|
264
|
+
* `/%61rchived`, walked past a `/archived` deny, and was dispatched as the
|
|
265
|
+
* record `archived` -- 200 with the record in full, and `DELETE` destroyed
|
|
266
|
+
* it, unauthenticated. 255 non-canonical spellings of an 8-character id
|
|
267
|
+
* decode to the same key, so a deny-list of spellings is the wrong shape.
|
|
268
|
+
*
|
|
269
|
+
* SO DO NOT NORMALISE THIS, AND DO NOT NORMALISE ANYTHING ELSE INSTEAD:
|
|
270
|
+
*
|
|
271
|
+
* - Do NOT decode it. Express decodes exactly ONCE, which is what a route
|
|
272
|
+
* parameter means. `GET /owners/%2561rchived` is the legitimate id
|
|
273
|
+
* `%61rchived`, not a second-order spelling of `archived`; a predicate that
|
|
274
|
+
* decoded until stable would deny a record it was never asked about.
|
|
275
|
+
* - Do NOT case-fold it. A record id is a VALUE, not a literal route segment,
|
|
276
|
+
* and express's `case sensitive routing` governs literal segments only.
|
|
277
|
+
* With a distinct owner seeded at `ARCHIVED`, `.toLowerCase()` was measured
|
|
278
|
+
* wrong in BOTH directions at once: `GET /owners/ARCHIVED` 403 (a false
|
|
279
|
+
* deny, on the wrong record) and `GET /owners/%41RCHIVED` 200 (a false
|
|
280
|
+
* allow, on that same record).
|
|
281
|
+
* - Do NOT derive it from `request.path` or the request target. Decoding the
|
|
282
|
+
* whole path decodes THEN splits, while the router splits THEN decodes, so
|
|
283
|
+
* `/owners/archived%2fx` -- a genuinely distinct record whose id is
|
|
284
|
+
* `archived/x` -- was measured over-denied 403.
|
|
285
|
+
*
|
|
286
|
+
* IT IS `getId(request.params)`, BYTE FOR BYTE -- the same single coercion
|
|
287
|
+
* the store lookup uses, exactly as `operation` is the same `methodAccessMap`
|
|
288
|
+
* lookup the permission-array branch uses. The predicate and the dispatch
|
|
289
|
+
* therefore cannot disagree about which record a request addresses. Handing
|
|
290
|
+
* over the raw `request.params.id` instead would reintroduce that divergence
|
|
291
|
+
* on hex-shaped ids: `GET /animals/0x2391` looks up record `9105`.
|
|
292
|
+
*
|
|
293
|
+
* It inherits abofs/stonyx-orm#209 along with that coercion -- on a model
|
|
294
|
+
* declaring `id = attr('string')`, `'9107'` arrives here as the number
|
|
295
|
+
* `9107`. That is consistency WITH THE LOOKUP, which is the property this key
|
|
296
|
+
* exists to buy; it is not a defect to repair here.
|
|
297
|
+
*
|
|
298
|
+
* `null`, not `undefined`, on a collection route -- and the KEY IS ALWAYS
|
|
299
|
+
* PRESENT, the same rule `operation` states above. `auth()` always sets it,
|
|
300
|
+
* so a context arriving WITHOUT the key did not come from `auth()`: it was
|
|
301
|
+
* hand-assembled by a caller resolving the predicate through
|
|
302
|
+
* `Orm.instance.getAccess()`. That absence stays a distinguishable, deniable
|
|
303
|
+
* signal only because the framework never produces it.
|
|
304
|
+
*
|
|
305
|
+
* IT DISAGREES WITH THE HOOK VOCABULARY, AND NOT ONLY ON THE ABSENCE
|
|
306
|
+
* SPELLING. `HookContext.recordId` (`src/hooks.ts`) is an identically-named
|
|
307
|
+
* key on an identically-shaped context object, which is the exact
|
|
308
|
+
* configuration that makes `operation` fail-open shaped -- a hook sees
|
|
309
|
+
* `'get'` where `access()` sees `'read'`. An earlier revision of THIS
|
|
310
|
+
* docblock asserted the opposite ("here they AGREE... they differ in ONE way
|
|
311
|
+
* and it is the absence spelling"). That was measured false, in the fail-open
|
|
312
|
+
* direction, and it is corrected here rather than deleted.
|
|
313
|
+
*
|
|
314
|
+
* MEASURED over the live dispatch, before-hooks registered for all five
|
|
315
|
+
* operations on one model:
|
|
316
|
+
*
|
|
317
|
+
* before:list key ABSENT ('recordId' in context === false)
|
|
318
|
+
* before:get key ABSENT params={"id":"visible1"}
|
|
319
|
+
* before:create key ABSENT
|
|
320
|
+
* before:update key ABSENT params={"id":"visible2"}
|
|
321
|
+
* before:delete recordId="visible3"
|
|
322
|
+
* after:delete recordId="visible3"
|
|
323
|
+
*
|
|
324
|
+
* `_withHooks` assigns `context.recordId` at exactly TWO sites in
|
|
325
|
+
* `src/orm-request.ts`, and BOTH sit inside an `operation === 'delete'`
|
|
326
|
+
* branch. So the two keys differ in COVERAGE, on four of five operations: on
|
|
327
|
+
* a hook context the key is absent for get, list, create and update, while
|
|
328
|
+
* this key is present on every route `auth()` classifies. The absence
|
|
329
|
+
* spelling is the smaller half of the difference, not the whole of it.
|
|
330
|
+
*
|
|
331
|
+
* AND THAT INVERTS THE ARGUMENT ABOVE WHEN IT IS READ ACROSS THE TWO. Here,
|
|
332
|
+
* a missing `recordId` means "did not come from `auth()`" and is deniable.
|
|
333
|
+
* On a hook context it means "this is a get / list / create / update" -- an
|
|
334
|
+
* ordinary request. A consumer who writes the hook-side half of the same
|
|
335
|
+
* rule --
|
|
336
|
+
*
|
|
337
|
+
* beforeHook('update', 'owner', ctx => ctx.recordId === 'archived' ? 403 : undefined)
|
|
338
|
+
*
|
|
339
|
+
* -- gets a deny that NEVER FIRES: measured, `PATCH /owners/visible2` -> 200,
|
|
340
|
+
* with `ctx.recordId === undefined` while the addressed record sits in
|
|
341
|
+
* `ctx.params`. The hook side is abofs/stonyx-orm#242 and is deliberately not
|
|
342
|
+
* repaired here. A predicate must not read `undefined` here as "collection",
|
|
343
|
+
* and nothing in this contract makes it safe to read the two keys as one key.
|
|
344
|
+
*
|
|
345
|
+
* IT NAMES WHICH RECORD, NOT WHICH SURFACE. `GET /owners/gina`,
|
|
346
|
+
* `GET /owners/gina/pets` and `GET /owners/gina/relationships/pets` all
|
|
347
|
+
* carry `recordId: 'gina'`; the related-resource gap is abofs/stonyx-orm#196
|
|
348
|
+
* and is untouched by this key.
|
|
349
|
+
*/
|
|
350
|
+
recordId: string | number | null;
|
|
255
351
|
}
|
|
256
352
|
|
|
257
353
|
/**
|