Skip to content

chore: land the cleanup campaign on main - #87

Merged
Upd4ting merged 25 commits into
mainfrom
chore/land-campaign-2026-11-04
Nov 4, 2026
Merged

Upd4ting merged 25 commits into
mainfrom
chore/land-campaign-2026-11-04

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 4, 2026

Copy link
Copy Markdown
Member

Lands campaign/integration at fa91e2d on main. Every change on that branch landed through a verified pull request. The tree of this pull request equals campaign/integration; the merge of main only joins the histories, since campaign/integration already contains every change of main up to 82ae1bb. No package is published by this merge.

Upd4ting and others added 22 commits October 31, 2026 00:25
* refactor: look up the index groups, container keys, floating values and schemas by own key

* refactor: read class prototypes through a checked helper

* refactor(modifiers): give the event handler alias no type parameter

* refactor(encryption): check that the auth-tag methods are callable

* refactor(table): collect the mixins without reading any

* refactor(database): insert the fixture rows without an any local

* test: read the stored rows, the module exports and the sort keys without any

* chore: lower the ratchet ceilings

* refactor(modifiers): type the receiver of the generated accessors

* chore: lower the no-unsafe-return ceiling

* refactor(records): look up the unlock arguments and the floating values by own key

* chore: lower the no-inherited-key-lookup ceiling

* refactor(table): assert the mixin subclass to the declared return type

* refactor(modifiers): restore the container unlock lookup to its base form
…the package (#55)

* test(metadata): pin the metadata stored by an older copy of the package

* fix(metadata): upgrade the table metadata stored by an older copy of the package
* test(modifiers): pin lookups by names shared with Object.prototype members

* fix(modifiers): look up a container entry by own key
* test(encryption): pin which stored objects are revived as dates

* fix(encryption): revive only the date marker the encoder writes

* refactor(encryption): type the stored values the date reviver reads
…ng it (#54)

* test(metadata): pin the metadata of subclasses of a table

* fix(metadata): copy the metadata a subclass inherits instead of sharing it

* test(metadata): pin the schema binding of a subclass built before RegisterSchema

* fix(metadata): bind a subclass to the schema registered under its schema name

* refactor(modifiers): copy the modifier lists of a parent from the record helpers

* refactor(modifiers): keep listsCopied private to the record helpers
* test(modifiers): pin modifier fields and events named like Object.prototype members

* fix(modifiers): look up modified fields and event names by own key

* test(modifiers): pin that a modifier on a field named __proto__ is refused

* fix(modifiers): refuse a modifier on a field named __proto__

* refactor(modifiers): keep the refused-field check with the record helpers
* docs: typecheck every example and drop hand-written version prose

* fix(docs): stop the docs program from emitting declarations
…t contracts (#62)

* test(modifiers): pin modifier fields and events named like Object.prototype members

* fix(modifiers): look up modified fields and event names by own key

* build: run pnpm 10.33.0

pnpm before 10.20 fails a frozen install of some file: tarball dependencies
(ENOENT in the store index), which the campaign tarball pins need.

* build(deps): test against the interface-database 0.3.0 tarball

Producer: AntelopeJS/interface-database feat/typed-query-stages at 4939b45, version 0.3.0.
The devDependency range ^0.1.11 excludes 0.3.0, so the contract suite never
ran against it.

* test: read the schema stage's id through interface-database 0.3.0's stage union

The recorded options are typed per stage now, so the tests match the schema
stage and its id instead of reading options.id off any stage.

* test(modifiers): pin that a modifier on a field named __proto__ is refused

* fix(modifiers): refuse a modifier on a field named __proto__

* refactor(modifiers): keep the refused-field check with the record helpers

* test: read the schema stage's id through interface-database 0.3.0's stage union in the subclass binding tests

The subclass binding tests landed after the 0.3.0 tarball pin was written, and
read options.id off any stage; the recorded options are typed per stage now.

* feat(database-decorators)!: type the fixture generator by its table class

Fixture now checks that the class it is called with is the decorated class or
a subclass of it, so it calls the generator with that class without an
assertion. The stored generator gives plain objects instead of records of
unknown values.

BREAKING CHANGE: DatumGeneratorOutput is `Row | Row[] | undefined` over
objects (`Row = object` by default) instead of records of unknown values.

* feat(database-decorators)!: type the model's schema name and index lookups

BasicDataModel keeps the schema name its table class registers into, which is
undefined for a table class without @RegisterTable; the type now says so
instead of asserting a string. getBy takes an index named by a string field of
the table and keys typed as interface-database's selection keys.

BREAKING CHANGE: DataModel.schemaName is `string | undefined`. getBy's index is
`Extract<keyof T, string>` and its keys are `IndexKey` (a value proxy or a
string, number or boolean), no longer `any`.

* feat(database-decorators)!: type the auto-date event handlers by the field they stamp

BREAKING CHANGE: AutoDateModifier.insert and update take
`(object: Partial<Record<F, Date>>, field: F)` instead of a record of unknown
values and a string.

* feat(database-decorators)!: refuse @Index on a symbol-keyed field

An index names a database field, so the decorator throws for a symbol key
instead of recording the symbol as a string.

BREAKING CHANGE: @Index on a symbol-keyed property throws at class
declaration.

* feat(database-decorators)!: drop the once-used type parameters of getModifiedFields and toDatabase

getModifiedFields takes any modifier class and toDatabase takes the table
instance as `{ constructor: any }`. getBy's IndexKey is derived from
Table.getAll instead of interface-database's common subpath.

BREAKING CHANGE: getModifiedFields has one type parameter (the object's type)
instead of two; toDatabase has none.

* feat(database-decorators)!: type the stored unlock arguments as unknown values

The arguments a modifier is unlocked or locked with are whatever the caller
passed for it, so ModifiersDynamicMetadata.fields records them as unknown
arrays and the engine no longer spreads `any` into modifier methods.

BREAKING CHANGE: ModifiersDynamicMetadata.fields is
`Record<string, Record<string, unknown[]>>` instead of `any[]` lists.

* feat(database-decorators)!: record each modifier event with its handler

attachModifier now stores the modifier method it found for an event next to
the field and the modifier, so triggerEvent calls that method with the
modifier as `this` instead of looking it up by name through two assertions.

BREAKING CHANGE: the entries of ModifiersStaticMetadata.events are
`[field, modified, handler]`, with the new exported ModifierEvent type for the
handler.

* build: set the package version to 0.2.0

* chore: lower the ratchet ceilings to the findings left after the contract changes

* test: assert the error type of the symbol-keyed index refusal, not its wording

* build(deps): test against interface-database 6bef278
…nd give (#67)

* feat(database-decorators)!: name the row forms the conversions read and give

A table row has three forms: the instance, the plain form and the stored
form. The conversions typed the last two as `any` or open dictionaries, which
hid what every caller already relies on. They now name them:

- `PlainData<T>` is what `fromPlainData` reads: every field optional and
  possibly partial, a field under a container modifier possibly its value per
  key, the plain form 40 consumer calls already write.
- `PlainRow<T>` is what `toPlainData` gives: the set fields of `T` by name.
- `StoredRow<T>` is what `toDatabase` gives: typed `T` as `Table<T>` reads it,
  with `_internal` declared.
- `fromDatabase` takes the row as `Table<T>` types it, and gives it back with
  the class's prototype.
- `update(obj)` without an id takes `_id` in its type, as the docs said.
- `DatumStaticMetadata.primary` is the literal `"_id"`: nothing writes it.
- `unlockrequest` reads the stored form through `ValueProxy#cast` instead of
  an `any`; its signature keeps its answers.

`_internal.data`, `floating` and the modifier state stop being `any`: what they
hold is known to the modifiers that wrote them, so they read as `unknown`.

BREAKING CHANGE: `fromDatabase`, `fromPlainData`, `toDatabase`, `toPlainData`
and the matching `BasicDataModel` statics no longer accept or return `any`.
`triggerEvent` loses a type parameter nothing reads. `primary` no longer takes
another string.

* feat(database-decorators)!: keep reads of a stored row unchecked

`fromDatabase` and the model's static take what the database answered, as
before. A typed parameter measured +2 `no-unsafe-*` rows in interface-data-api
and three compile breaks, and it cleared nothing here: the two rows that stay
are the driver boundary, and an `as` in their place would only trade one
finding for another. `triggerEvent` keeps its type parameter for the same
reason: without it, that `any` copy is a new `no-unsafe-argument`.

`StoredRow<T>` is no longer `T`: a stored row has no prototype, so typing it as
the class made every spread of it a `no-misused-spread`. It now reads as the
set fields. The model's own writes keep the `T` they build.

* fix(modifiers): keep assigning a floating value by a plain assignment's rules

* chore(suppressions): lower the SAFETY budget to the comments left
… typed query operands (#70)

* build(deps): pin interface-database to the query value tarball

* feat(database-decorators)!: take a text field in unlockrequest

`unlockrequest` read the proxy of a field by `keyof T`, which includes
numbers and symbols. A query operand is a name or a number held in a
query value, never a symbol, so the field is a text key.

BREAKING CHANGE: `unlockrequest` takes `K extends keyof T & string`.

* chore(ratchet): leave the ceilings to the ceiling pass

* build(deps): pin interface-database to the structural operand tarball

* build(deps): pin interface-database to the IsQueryValue tarball

* build(deps): pin interface-database to the type-only tarball

* fix(database-decorators): keep unlockrequest's published key constraint

Narrow the key to text inside unlockrequest instead of tightening K, so interface-data-api's Validation.UnlockRequest<T, K extends keyof T> keeps compiling.
…what a decorator holds (#71)

* refactor(database-decorators): build on interface-core 0.4's typed decorator targets

The table modifier decorators get a DecoratorTarget, whose constructor is
a Function, so they attach through an internal attachModifierTo that
takes the class as a Function and the field as a property key; the
public attachModifier keeps its typed signature and delegates to it.
RegisterTable's handler receives a Class<Table> through MakeClassDecorator's
class parameter. unlockrequest takes any Constructible table, since it
only reads the table's modifier metadata, so a caller holding a
Class<object> no longer has to claim the row type. Building a modifier's
field entry moves to internal/records.

Pins interface-core to the 6a8b526 tarball (0.4.0).

* feat(database-decorators): let attachModifier take the class a decorator receives

attachModifier takes the table class as a Function and the field as a
property key, what it reads (the class's metadata, name and prototype),
so a property decorator can pass its DecoratorTarget's constructor and
key as they come, as the docs show. The internal attachModifierTo this
branch added is gone: the table modifiers call attachModifier again, and
the package gains no runtime export.
* chore(ratchet): delete the guardrail ratchet

* build(sync): drop the ratchet step
* docs(readme): use one Discord invite (#68)

* docs(github): send questions and ideas to Discord (#76)

* fix(fixtures): let two processes register a fixture on an empty table (#74)

* test(fixtures): cover two processes registering a fixture at once

* fix(fixtures): let two processes register a fixture on an empty table

* docs(fixtures): keep to what the fixture insert checks

* Merge commit from fork

* fix(model): keep modifier data on a partial update of another field

A modifier event creates an empty `_internal` on the instance. The update
writes that key as one value, so it erased what the row stored there.

* fix(hash): hash values with scrypt by default

`@Hashed()` now stores `$scrypt$ln=15,r=8,p=1$<salt>$<key>`. Rows in the
earlier digest format still verify, and `isHashOutdated` flags them so that a
caller can write them again after a successful test. `testHashAsync` runs the
test on the thread pool. An explicit `algorithm` keeps its digest format.

* fix(hash): raise the default scrypt cost to p=3

N=2^15, r=8, p=3 is the minimum OWASP lists for that N. It takes about 150 ms
and the same 32 MiB as p=1.

* test(hash): give suites that spend scrypt runs a longer limit

At p=3 a test with several runs passes mocha's 2 s limit on a busy machine.

* fix(hash): refuse a stored hash outside the cost bounds

A stored hash names its own cost, so a tampered row could ask for an expensive
run. Salt and key must take 16 to 64 bytes, the memory 128 MiB at most and p 16
at most. A row outside that, or with an N that r cannot hold, verifies nothing
and no longer throws.

* chore(release): v0.1.12

* docs(hash): mark the stored-hash upgrade snippet as a fragment

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: pass the database-decorators docs to the docs standard

* docs: turn on the docs standard

* docs: say when fromDatabase returns undefined and when a digest test runs scrypt
@gitguardian

gitguardian Bot commented Nov 4, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
39448498 Triggered Generic Password 4a959c7 src/tests/helpers/hash-rows.ts View secret
39448499 Triggered Generic Password 4a959c7 src/tests/helpers/hash-rows.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Upd4ting
Upd4ting force-pushed the chore/land-campaign-2026-11-04 branch from 3176468 to 69ef969 Compare November 4, 2026 19:51
@Upd4ting
Upd4ting merged commit 921bf9d into main Nov 4, 2026
3 of 7 checks passed
@Upd4ting
Upd4ting deleted the chore/land-campaign-2026-11-04 branch November 4, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant