@stonyx/orm 0.3.2-alpha.63 → 0.3.2-alpha.65

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.
Files changed (2) hide show
  1. package/README.md +71 -11
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -629,15 +629,50 @@ record-level routes are ungated, so the id was the only thing standing between
629
629
  an unauthenticated caller and `GET`/`PATCH`/`DELETE` on a record. That was never
630
630
  a control and must not become one; configure `access`.
631
631
 
632
- **And the id itself is an occupancy signal.** `assignRecordId` reads the whole
633
- store, not the caller's filtered view — it never sees `state.filter` — so the id
634
- it returns is a function of records the caller may not be permitted to read. On
635
- a string-id collection the assigned `n` is the smallest positive integer whose
636
- key is free, which tells the caller that every key below it is taken, hidden or
637
- not. Closing that requires the assignment to be filter-aware, which is a change
638
- to the `access` contract rather than a fix; it is stated here rather than left
639
- to be discovered. Callers with no function-style filter are unaffected there
640
- are no hidden records to disclose.
632
+ **And the id itself is an occupancy signal on both model kinds.**
633
+ `assignRecordId` reads the whole store, not the caller's filtered view — it
634
+ never sees `state.filter` — so the id it returns is a function of records the
635
+ caller may not be permitted to read. **This applies to numeric-id collections
636
+ as well as string-id ones**, and the conditions differ, so read both:
637
+
638
+ - **String-id collections, always.** The assigned `n` is the smallest positive
639
+ integer whose landing key is free, which tells the caller that every key
640
+ below it is taken, hidden or not.
641
+ - **Numeric-id collections, once one record sits at the numeric ceiling.** The
642
+ normal answer is `max + 1`, which discloses only the maximum. But `max + 1`
643
+ is not representable at or above 2^53, so the walk restarts from `1` (see
644
+ breaking change 8) and the assigned id becomes the smallest free integer —
645
+ the same occupancy predicate, now over arbitrary low keys. Each subsequent
646
+ no-id `POST` names the next free one, so a caller can enumerate the holes in
647
+ a range it cannot read.
648
+
649
+ **A ceiling record reaches a filter-protected collection even though `POST`
650
+ refuses caller ids on one.** Breaking change 3 makes
651
+ `POST /animals {"id": 9007199254740992}` answer `403`, but the same id lands
652
+ through a *relationship write on another collection* —
653
+ `POST /owners` carrying `attributes: { pets: [{ "id": 9007199254740992 }] }`
654
+ creates the animal under that key
655
+ ([#207](https://github.com/abofs/stonyx-orm/issues/207), the same channel the
656
+ **Known limitations** re-parenting note describes). So the precondition is
657
+ reachable by an unauthenticated caller on exactly the collections `access`
658
+ exists to protect. Measured on the sample fixture, with every animal hidden by
659
+ the `/animals` predicate and keys 4 and 7 deleted:
660
+
661
+ ```
662
+ GET /animals -> 200 [] (nothing visible)
663
+ GET /animals/4 -> 404 (free — indistinguishable from hidden)
664
+ POST /animals {"id":4} -> 403 (breaking change 3)
665
+ POST /owners {... pets:[{"id":9007199254740992}]} -> 200 (#207 plants the ceiling record)
666
+ POST /animals (no id) -> 200 id=4 <- names a hole in the hidden range
667
+ POST /animals (no id) -> 200 id=7 <- and the other one
668
+ POST /animals (no id) -> 200 id=13
669
+ POST /animals (no id) -> 200 id=14
670
+ ```
671
+
672
+ Closing this requires the assignment to be filter-aware, which is a change to
673
+ the `access` contract rather than a fix; it is stated here rather than left to
674
+ be discovered. Callers with no function-style filter are unaffected — there are
675
+ no hidden records to disclose.
641
676
 
642
677
  ### Identifying the collection
643
678
 
@@ -848,8 +883,9 @@ they are recorded here.
848
883
  population breaking changes 3 and 4 explicitly exempt. If you were relying on
849
884
  a hex-shaped or whitespace-padded id creating a second record, it never did.
850
885
 
851
- 8. **Server-assigned ids change value on string-id models, and the create route
852
- gains a `409`.** Two consumer-visible changes from
886
+ 8. **Server-assigned ids change value on string-id models, numeric ids stop
887
+ being monotonic at the numeric ceiling, and the create route gains a
888
+ `409`.** Three consumer-visible changes from
853
889
  [#203](https://github.com/abofs/stonyx-orm/issues/203).
854
890
 
855
891
  **The value.** A `POST` with no `id` against a model declaring
@@ -862,6 +898,7 @@ they are recorded here.
862
898
  id models (`id = attr('number')`, the default) are unaffected in shape: they
863
899
  still get an integer, but it is now the **maximum** existing id plus one
864
900
  rather than the last-inserted id plus one, which is the defect #203 is about.
901
+ They are **not** unaffected in *sequence* — see the monotonicity half below.
865
902
 
866
903
  The value is deliberately **not** numeric-looking, and that is not cosmetic.
867
904
  Every id-bearing surface resolves a numeric-looking string id to a **number**
@@ -872,6 +909,29 @@ they are recorded here.
872
909
  `context.record === undefined`
873
910
  ([#209](https://github.com/abofs/stonyx-orm/issues/209)).
874
911
 
912
+ **Numeric ids are no longer monotonic, and deleted ids can be re-issued.**
913
+ The precondition is narrow but it is reachable, and there is no signal when
914
+ it is met: **one record filed at or above 2^53** (`9007199254740992`). `max
915
+ + 1` is not representable there, so assignment restarts from `1` and walks
916
+ up to the lowest free key — which means the id of a *deleted* record is
917
+ handed to the next `POST`. Both `dev` and every prior release were strictly
918
+ monotonic and never re-issued a numeric id, so a consumer that relied on
919
+ that — audit rows, cursors, cached authorization decisions, external
920
+ references keyed on the id — now has a stale reference that silently points
921
+ at a **different record, created by a different caller**, rather than at a
922
+ deleted one. Nothing fails; the reference simply resolves to the wrong
923
+ record.
924
+
925
+ The restart is deliberate and is not itself optional: without it, one record
926
+ at the ceiling made every subsequent server-assigned create on that
927
+ collection fail permanently. Re-use is the cost of keeping the collection
928
+ writable. **If you need monotonic ids, assign them yourself** rather than
929
+ letting the server assign, and note that a ceiling record can be planted by
930
+ an unauthenticated caller — see *And the id itself is an occupancy signal*
931
+ under [Filter functions](#filter-functions) for the reachability path.
932
+ String-id models are unaffected by this half: their keys are
933
+ `<model>-<n>` and were never monotonic over an integer sequence.
934
+
875
935
  **The status.** `POST /{collection}` can now answer `409` for a reason other
876
936
  than a duplicate id: the server could not derive a free id. That requires a
877
937
  **non-injective** id transform — one that maps distinct candidates onto the
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-alpha.63",
7
+ "version": "0.3.2-alpha.65",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",