diff --git a/package.json b/package.json
index ab9296b..4c797f2 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"@astrojs/check": "^0.9.8",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
- "astro": "^6.1.8",
+ "astro": "^6.1.9",
"date-fns": "^4.1.0",
"remark-smartypants": "^3.0.2",
"sharp": "^0.34.5"
@@ -44,6 +44,6 @@
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^6.0.3",
- "typescript-eslint": "^8.58.2"
+ "typescript-eslint": "^8.59.0"
}
}
diff --git a/src/content.config.ts b/src/content.config.ts
index cb36185..1a1c5dc 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -13,9 +13,22 @@ const posts = defineCollection({
date: z.coerce.date(),
draft: z.boolean().default(false),
excerpt: z.string().nullable().optional(),
+ deprecated: z.boolean().default(false),
+ supersededBy: z.string().nullable().optional(),
}),
});
+const docs = defineCollection({
+ loader: glob({ pattern: '**/*.md', base: './src/content/docs' }),
+ schema: z.object({
+ title: z.string(),
+ description: z.string(),
+ package: z.string().optional(),
+ lastVerified: z.coerce.date(),
+ order: z.number().default(0),
+ }),
+});
+
const authors = defineCollection({
loader: file('src/data/authors.yaml'),
schema: z.object({
@@ -39,4 +52,4 @@ const tags = defineCollection({
}),
});
-export const collections = { posts, authors, tags };
+export const collections = { posts, docs, authors, tags };
diff --git a/src/content/docs/data-stores.md b/src/content/docs/data-stores.md
new file mode 100644
index 0000000..05e5a41
--- /dev/null
+++ b/src/content/docs/data-stores.md
@@ -0,0 +1,168 @@
+---
+title: Data Stores
+description: Declare physical stores as DI tokens that carry model and primary-key metadata, with a dedicated helper per backend.
+package: '@furystack/core'
+lastVerified: '2026-04-25T00:00:00.000Z'
+order: 2
+---
+
+A **physical store** is the minimal interface for persisting a collection of entities — create, read, filter, update, delete, count. Stores don't know about authorization, relations, or business logic. That lives one layer up, in [DataSets](/getting-started/repository/).
+
+A **`StoreToken`** is a DI token that resolves to a physical store and carries the store's `model` and `primaryKey` as metadata. Backend adapter packages ship dedicated helpers that mint the right token for their flavour.
+
+## Declaring a store
+
+```ts
+import { defineStore, InMemoryStore } from '@furystack/core';
+
+class TodoItem {
+ declare id: string;
+ declare title: string;
+ declare completed: boolean;
+}
+
+export const TodoStore = defineStore({
+ name: 'my-app/TodoStore',
+ model: TodoItem,
+ primaryKey: 'id',
+ factory: () => new InMemoryStore({ model: TodoItem, primaryKey: 'id' }),
+});
+```
+
+`defineStore` wraps `defineService({ lifetime: 'singleton' })` and registers an `onDispose` so the store cleans itself up when the injector is disposed.
+
+> **Tip:** Pass the generics explicitly (``). Inferred generics inside helper wrappers tend to widen `'id'` back to `keyof T`, which loses the literal primary-key type.
+
+## Resolving the store
+
+```ts
+import { createInjector } from '@furystack/inject';
+
+const injector = createInjector();
+const store = injector.get(TodoStore);
+await store.add({ id: '1', title: 'first', completed: false });
+```
+
+> **Heads up:** Resolving a `StoreToken` directly in application code is a smell — data should flow through DataSets, not raw stores. The `furystack/no-direct-store-token` lint rule flags this. See [Repository](/getting-started/repository/).
+
+## Backend adapters
+
+Each adapter exports a `defineXxxStore(opts)` helper:
+
+| Package | Helper | Use when… |
+| ----------------------------- | ----------------------- | --------------------------------------------------- |
+| `@furystack/core` | `InMemoryStore` factory | Tests, demos, ephemeral state. |
+| `@furystack/filesystem-store` | `defineFileSystemStore` | Development, prototypes, low-volume persistence. |
+| `@furystack/sequelize-store` | `defineSequelizeStore` | Any SQL DB via Sequelize (Postgres, MySQL, SQLite). |
+| `@furystack/mongodb-store` | `defineMongoDbStore` | MongoDB document storage. |
+| `@furystack/redis-store` | `defineRedisStore` | Redis key-value storage (e.g. shared sessions). |
+
+### `defineFileSystemStore`
+
+```ts
+import { defineFileSystemStore } from '@furystack/filesystem-store';
+
+export const TodoStore = defineFileSystemStore({
+ name: 'my-app/TodoStore',
+ model: TodoItem,
+ primaryKey: 'id',
+ fileName: './data/todos.json',
+ tickMs: 5000, // Optional: throttle disk writes to once per N ms
+});
+```
+
+### `defineMongoDbStore`
+
+```ts
+import { defineMongoDbStore } from '@furystack/mongodb-store';
+
+export const TodoStore = defineMongoDbStore({
+ name: 'my-app/TodoStore',
+ model: TodoItem,
+ primaryKey: 'id',
+ url: process.env.MONGODB_URL!,
+ db: 'my-app',
+ collection: 'todos',
+});
+```
+
+The shared `MongoClientFactory` token pools clients per URL and closes them all on injector teardown — no connection leaks across tests.
+
+### `defineRedisStore`
+
+```ts
+import { createClient } from 'redis';
+import { defineRedisStore } from '@furystack/redis-store';
+
+const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
+
+export const SessionStore = defineRedisStore({
+ name: 'my-app/SessionStore',
+ model: Session,
+ primaryKey: 'sessionId',
+ client: redisClient,
+});
+```
+
+The caller owns the `redis` client lifecycle — connect at startup, `quit()` on shutdown.
+
+### `defineSequelizeStore`
+
+```ts
+import { DataTypes, Model } from 'sequelize';
+import { defineSequelizeStore } from '@furystack/sequelize-store';
+
+class TodoModel extends Model {}
+
+export const TodoStore = defineSequelizeStore({
+ name: 'my-app/TodoStore',
+ model: TodoItem,
+ sequelizeModel: TodoModel,
+ primaryKey: 'id',
+ options: { dialect: 'postgres' /* ... */ },
+ initModel: ({ model }) => {
+ model.init(
+ {
+ id: { type: DataTypes.STRING, primaryKey: true },
+ title: DataTypes.STRING,
+ completed: DataTypes.BOOLEAN,
+ },
+ { sequelize: model.sequelize!, tableName: 'todos' },
+ );
+ },
+});
+```
+
+The shared `SequelizeClientFactory` token pools clients per `JSON.stringify(options)` key and disposes them on teardown.
+
+## Throw-by-default stores
+
+Some packages ship `StoreToken`s that **throw** when resolved without a binding. This is intentional — they represent stores the framework needs but cannot pick on the application's behalf. Examples: `UserStore` and `SessionStore` from `@furystack/rest-service`; `RefreshTokenStore` from `@furystack/auth-jwt`; `PasswordCredentialStore` and `PasswordResetTokenStore` from `@furystack/security`.
+
+Bind a concrete implementation at app bootstrap:
+
+```ts
+import { UserStore } from '@furystack/rest-service';
+import { defineSequelizeStore } from '@furystack/sequelize-store';
+
+const AppUserStore = defineSequelizeStore({
+ name: 'my-app/AppUserStore',
+ model: User,
+ sequelizeModel: UserModel,
+ primaryKey: 'username',
+ options: { dialect: 'postgres' /* ... */ },
+});
+
+injector.bind(UserStore, ctx => ctx.inject(AppUserStore));
+```
+
+In tests, bind an `InMemoryStore` per scope:
+
+```ts
+injector.bind(UserStore, () => new InMemoryStore({ model: User, primaryKey: 'username' }));
+```
+
+## Where to look next
+
+- [Repository](/getting-started/repository/) — wrap a `StoreToken` in a `DataSet` with authorization and events.
+- [Dependency Injection](/getting-started/inject/) — how `defineService` and tokens work in general.
diff --git a/src/content/docs/data-validation.md b/src/content/docs/data-validation.md
new file mode 100644
index 0000000..f7d7be4
--- /dev/null
+++ b/src/content/docs/data-validation.md
@@ -0,0 +1,117 @@
+---
+title: Data Validation
+description: Generate JSON Schemas from your TypeScript REST API and enforce them at runtime with the Validate middleware.
+package: '@furystack/rest-service'
+lastVerified: '2026-04-25T00:00:00.000Z'
+order: 5
+---
+
+A type-safe REST API gives you compile-time confidence that the server and client agree on shapes. It does **not** stop a malformed request from reaching your handler at runtime — TypeScript types are erased before the bytes hit the wire.
+
+The fix is to project the same TypeScript interfaces into runtime-checkable JSON Schemas, and run incoming payloads through them with the `Validate` middleware from `@furystack/rest-service`.
+
+## The plan
+
+1. Define the API interface (see [REST](/getting-started/rest/)).
+2. Generate a JSON Schema from those interfaces with `ts-json-schema-generator`.
+3. Wrap each endpoint handler with `Validate({ schema, schemaName })`.
+
+## 1. Generate the schema
+
+Add `ts-json-schema-generator` as a dev dependency in your `common` workspace:
+
+```sh
+yarn add -D ts-json-schema-generator
+```
+
+Add an npm script that emits a schema for the file containing your endpoint types:
+
+```json
+{
+ "scripts": {
+ "build:schema": "ts-json-schema-generator -f tsconfig.json --no-type-check -p src/api.ts -o src/api.schema.json"
+ }
+}
+```
+
+Then re-export it so both server and client can import it as JSON:
+
+```ts
+// common/src/index.ts
+import schema from './api.schema.json' with { type: 'json' };
+export { schema };
+```
+
+Make sure `resolveJsonModule: true` is in your `tsconfig.json`.
+
+## 2. Validate on the server
+
+Wrap the endpoint handler with `Validate({ schema, schemaName })`. The `schemaName` should match the type the request payload conforms to.
+
+```ts
+import type { RequestAction } from '@furystack/rest-service';
+import { JsonResult, Validate } from '@furystack/rest-service';
+import { schema } from 'common';
+import type { CreateTodo } from 'common';
+
+const createTodoEndpoint: RequestAction = Validate({
+ schema,
+ schemaName: 'CreateTodo',
+})(async ({ getBody, injector }) => {
+ const body = await getBody();
+ const dataSet = injector.get(TodoDataSet);
+ const todo = await dataSet.add(injector, body);
+ return JsonResult(todo);
+});
+```
+
+`Validate` checks **every** field of the request — `body`, `query`, `url`, `headers`. If something doesn't match the schema, the middleware short-circuits with a `400 Bad Request` and a verbose `ajv` error message describing exactly what failed. Your handler never runs with bad data.
+
+## 3. Wire it into the API
+
+Drop the validated handler into your `useRestService` call like any other endpoint:
+
+```ts
+import { useRestService } from '@furystack/rest-service';
+
+await useRestService({
+ injector,
+ port: 3000,
+ root: '/api',
+ api: {
+ POST: {
+ '/todos': createTodoEndpoint,
+ },
+ },
+});
+```
+
+## What gets validated
+
+Everything declared on the endpoint type:
+
+```ts
+export interface CreateTodo {
+ body: { title: string; description?: string };
+ query: { dryRun?: boolean };
+ result: { id: string };
+}
+```
+
+The `Validate` middleware enforces:
+
+- Required fields (`title` must be present)
+- Type correctness (`title` must be a string, `dryRun` must be a boolean)
+- Optionality (missing `description` is fine; missing `title` is a `400`)
+- Nested objects, unions, intersections, string literals, regex constraints — anything `ts-json-schema-generator` can express.
+
+## Tips
+
+- **Add `additionalProperties: false`** to your schema if you want unknown fields to fail validation. By default they pass through silently.
+- **Regenerate the schema in CI** alongside your other build steps — it's a sync between two source-of-truth descriptions of your API, and a stale schema can hide drift.
+- **Reuse the schema on the client** for early form validation if you want — but the server is still the trust boundary.
+
+## Where to look next
+
+- [REST](/getting-started/rest/) — designing the API interface that the schema is generated from.
+- [Repository](/getting-started/repository/) — DataSet authorization callbacks pair naturally with `Validate` for "is this payload well-formed _and_ are you allowed to do this?"
diff --git a/src/content/docs/inject.md b/src/content/docs/inject.md
new file mode 100644
index 0000000..4d84582
--- /dev/null
+++ b/src/content/docs/inject.md
@@ -0,0 +1,135 @@
+---
+title: Dependency Injection
+description: Define services as factories behind tokens, resolve them through an injector with explicit lifetimes and disposable scopes.
+package: '@furystack/inject'
+lastVerified: '2026-04-25T00:00:00.000Z'
+order: 1
+---
+
+`@furystack/inject` is the dependency-injection primitive every other FuryStack package builds on. Services are declared with `defineService` (or `defineServiceAsync`), which returns an opaque **token**. The injector resolves tokens, caches instances per lifetime, and runs disposal callbacks on teardown. There are no decorators.
+
+## A first service
+
+```ts
+import { createInjector, defineService } from '@furystack/inject';
+
+const Counter = defineService({
+ name: 'my-app/Counter',
+ lifetime: 'singleton',
+ factory: () => {
+ let value = 0;
+ return { increment: () => ++value };
+ },
+});
+
+const injector = createInjector();
+const counter = injector.get(Counter);
+counter.increment(); // 1
+counter.increment(); // 2
+```
+
+`Counter` does double duty — it's both the token (passed to `injector.get`) and the type (annotate variables with it). Each `defineService` call mints a fresh `Symbol` for identity, so two unrelated services with the same `name` never collide.
+
+## Dependencies and disposal
+
+Factories receive a `ServiceContext` with `inject`, `injector`, and `onDispose`:
+
+```ts
+import { defineService } from '@furystack/inject';
+
+const PaymentService = defineService({
+ name: 'my-app/PaymentService',
+ lifetime: 'singleton',
+ factory: ({ inject, onDispose }) => {
+ const settings = inject(PaymentSettings);
+ const client = createPaymentClient(settings);
+ onDispose(async () => client.close());
+ return {
+ charge: async (amount: number) => client.charge(amount),
+ };
+ },
+});
+```
+
+`onDispose` callbacks fire in **reverse registration order** when the owning injector is disposed. Use them for any teardown — closing pools, removing event listeners, releasing scopes.
+
+## Lifetimes
+
+Three lifetimes:
+
+- **`singleton`** — one instance per root injector. The default for shared, stateless services (settings, telemetry hubs, client pools).
+- **`scoped`** — one instance per scope. Good for per-request, per-message, or per-test state.
+- **`transient`** — a fresh instance for every `injector.get(...)`. Rarely what you want.
+
+```ts
+const RequestLogger = defineService({
+ name: 'my-app/RequestLogger',
+ lifetime: 'scoped',
+ factory: ({ inject }) => {
+ const headers = inject(RequestHeaders);
+ return createLogger({ requestId: headers['x-request-id'] });
+ },
+});
+```
+
+## Scopes
+
+`createScope({ owner })` creates a child injector with its own cache. Use it for per-request, per-message, or per-test isolation:
+
+```ts
+import { createInjector } from '@furystack/inject';
+
+const root = createInjector();
+
+await using request = root.createScope({ owner: 'request-42' });
+const logger = request.get(RequestLogger); // resolved once on `request`, gone when disposed
+```
+
+The `await using` (TC39 explicit resource management) syntax disposes the scope automatically when the block exits — running every `onDispose` callback in LIFO order. The same holds for the root injector itself.
+
+## Overriding services
+
+Two operations let you swap implementations at runtime — for tests, or for binding throw-by-default tokens to your application's persistent stores:
+
+- **`bind(token, factory)`** — replace the factory on the injector that owns the cached instance, dropping any cached value.
+- **`invalidate(token)`** — clear the cache for a token without rebinding.
+
+```ts
+import { LoggerCollection } from '@furystack/logging';
+
+const injector = createInjector();
+injector.bind(LoggerCollection, () => createTestLogger());
+```
+
+This is the canonical replacement for the deprecated `setExplicitInstance` pattern.
+
+## Async services
+
+When construction needs `await`, use `defineServiceAsync`. The factory returns a `Promise`; consumers resolve via `injector.getAsync(Token)`:
+
+```ts
+import { defineServiceAsync } from '@furystack/inject';
+
+const ConfigService = defineServiceAsync({
+ name: 'my-app/ConfigService',
+ lifetime: 'singleton',
+ factory: async () => {
+ const raw = await readFile('./config.json', 'utf8');
+ return JSON.parse(raw);
+ },
+});
+
+const config = await injector.getAsync(ConfigService);
+```
+
+Sync `injector.get(token)` rejects async tokens at compile time — no runtime surprises.
+
+## What about `init(injector)`?
+
+The previous-generation injector silently called `init(injector)` on freshly constructed singletons that exposed it. The current injector does **not**. If your service needs async bootstrap, convert it to `defineServiceAsync`. Hide loaders inside the factory body; consumers should receive a fully-initialised instance.
+
+## Where to look next
+
+- [Data Stores](/getting-started/data-stores/) — declarative store tokens with `defineStore`.
+- [Repository](/getting-started/repository/) — DataSets with authorization, hooks, and events.
+- [Migration guide](https://github.com/furystack/furystack/blob/develop/docs/migrations/v7-functional-di.md) — full API delta from the previous decorator-based API.
diff --git a/src/content/docs/repository.md b/src/content/docs/repository.md
new file mode 100644
index 0000000..eb5b251
--- /dev/null
+++ b/src/content/docs/repository.md
@@ -0,0 +1,141 @@
+---
+title: Repository
+description: DataSet tokens layer authorization, hooks, and events on top of physical stores.
+package: '@furystack/repository'
+lastVerified: '2026-04-25T00:00:00.000Z'
+order: 3
+---
+
+A **DataSet** wraps a physical store with entity-level business logic — authorization callbacks, modification hooks, and change events. Where a `PhysicalStore` only handles CRUD, a `DataSet` enforces _who_ may do _what_, _when_, and _what gets emitted_ as a side effect.
+
+Use the DataSet for every write to an entity. The `furystack/no-direct-store-token` lint rule flags direct store access in application code precisely because bypassing the DataSet skips authorization, modifiers, and downstream subscribers (e.g. [entity-sync-service](https://github.com/furystack/furystack/tree/develop/packages/entity-sync-service)).
+
+## Declaring a DataSet
+
+```ts
+import { createInjector } from '@furystack/inject';
+import { defineStore, InMemoryStore } from '@furystack/core';
+import { defineDataSet, getDataSetFor } from '@furystack/repository';
+import { getLogger } from '@furystack/logging';
+
+class TodoItem {
+ declare id: string;
+ declare title: string;
+ declare completed: boolean;
+}
+
+const TodoStore = defineStore({
+ name: 'my-app/TodoStore',
+ model: TodoItem,
+ primaryKey: 'id',
+ factory: () => new InMemoryStore({ model: TodoItem, primaryKey: 'id' }),
+});
+
+export const TodoDataSet = defineDataSet({
+ name: 'my-app/TodoDataSet',
+ store: TodoStore,
+ settings: {
+ authorizeAdd: async ({ entity }) => {
+ if (!entity.title || entity.title.length < 3) {
+ return {
+ isAllowed: false,
+ message: 'Todo title must be at least 3 characters.',
+ };
+ }
+ return { isAllowed: true };
+ },
+ onEntityAdded: ({ injector, entity }) => {
+ getLogger(injector).verbose({
+ message: `Todo added: ${entity.title}`,
+ });
+ },
+ },
+});
+```
+
+`defineDataSet` returns a `DataSetToken` that carries the model and primary-key metadata along with the configured behaviour.
+
+## Using a DataSet
+
+```ts
+const injector = createInjector();
+const dataSet = getDataSetFor(injector, TodoDataSet);
+
+await dataSet.add(injector, { id: '1', title: 'walk the cat', completed: false });
+const todos = await dataSet.find(injector, { filter: { completed: { $eq: false } } });
+```
+
+`getDataSetFor` is a convenience wrapper around `injector.get(TodoDataSet)`. Pick whichever reads cleaner at the call site.
+
+## Authorization callbacks
+
+Each callback returns an `AuthorizationResult` (`{ isAllowed: true }` or `{ isAllowed: false, message: string }`).
+
+| Callback | Fires before… |
+| ----------------------- | --------------------------------------------- |
+| `authorizeAdd` | inserting a new entity |
+| `authorizeUpdate` | updating an entity (without loading it first) |
+| `authorizeUpdateEntity` | updating an entity (loads the original) |
+| `authorizeRemove` | deleting (without loading) |
+| `authorizeRemoveEntity` | deleting (loads the entity) |
+| `authorizeGet` | reading a collection |
+| `authorizeGetEntity` | reading a single entity |
+
+The `Entity` variants (re)load the persisted entity before the check, so you can compare old vs new values or check ownership. Skip them when you only need to validate the incoming payload.
+
+## Modifiers and additional filters
+
+- **`modifyOnAdd` / `modifyOnUpdate`** — transform the entity before it is persisted. Useful for `createdByUser`, `lastModifiedAt`, etc.
+- **`addFilter`** — append a condition to every collection query, narrowing what callers can see.
+
+```ts
+defineDataSet({
+ name: 'my-app/TodoDataSet',
+ store: TodoStore,
+ settings: {
+ modifyOnAdd: async ({ injector, entity }) => {
+ const httpUser = injector.get(HttpUserContext);
+ const user = await httpUser.getCurrentUser();
+ return { ...entity, createdByUser: user.username };
+ },
+ addFilter: async ({ injector, filter }) => {
+ const httpUser = injector.get(HttpUserContext);
+ const user = await httpUser.getCurrentUser();
+ return { ...filter, createdByUser: { $eq: user.username } };
+ },
+ },
+});
+```
+
+## Change events
+
+`onEntityAdded`, `onEntityUpdated`, and `onEntityRemoved` fire after the operation succeeds. Use them for logging, metrics, cache invalidation, or pushing changes to subscribers.
+
+## Server-side writes (background jobs, seed scripts)
+
+Code outside an HTTP request has no `HttpUserContext`. Use `useSystemIdentityContext` to create a scoped child injector with elevated privileges, and dispose it via `usingAsync`:
+
+```ts
+import { useSystemIdentityContext } from '@furystack/core';
+import { getDataSetFor } from '@furystack/repository';
+import { usingAsync } from '@furystack/utils';
+
+await usingAsync(
+ useSystemIdentityContext({ injector, username: 'seed-script' }),
+ async systemInjector => {
+ const dataSet = getDataSetFor(systemInjector, TodoDataSet);
+ await dataSet.add(systemInjector, {
+ id: 'seed-1',
+ title: 'first seed',
+ completed: false,
+ });
+ },
+);
+```
+
+> **Warning:** `useSystemIdentityContext` bypasses every authorization callback. Only use it in trusted server-side code. Never hand the returned injector to a user-facing request handler.
+
+## Where to look next
+
+- [Data Stores](/getting-started/data-stores/) — declaring the underlying `StoreToken`.
+- [REST](/getting-started/rest/) — wire a DataSet to CRUD endpoints in two lines.
diff --git a/src/content/docs/rest.md b/src/content/docs/rest.md
new file mode 100644
index 0000000..550088c
--- /dev/null
+++ b/src/content/docs/rest.md
@@ -0,0 +1,183 @@
+---
+title: REST
+description: Type-safe REST API contracts shared between server and client, with endpoint generators driven by DataSet tokens.
+package: '@furystack/rest'
+lastVerified: '2026-04-25T00:00:00.000Z'
+order: 4
+---
+
+FuryStack splits REST into three packages so the same TypeScript interface drives the server, the client, and any tooling in between:
+
+| Package | Role |
+| ------------------------------ | --------------------------------------------------------------- |
+| `@furystack/rest` | Define the API as a TypeScript interface (no runtime). |
+| `@furystack/rest-service` | Implement the API server-side, with optional auth and CORS. |
+| `@furystack/rest-client-fetch` | Call the API from the browser (or Node 22+) via native `fetch`. |
+
+Share the API interface from a `common` workspace; both ends import the same type.
+
+## 1. Design the API
+
+```ts
+// common/src/index.ts
+import type {
+ RestApi,
+ GetCollectionEndpoint,
+ GetEntityEndpoint,
+ PostEndpoint,
+ PatchEndpoint,
+ DeleteEndpoint,
+} from '@furystack/rest';
+
+export interface TodoItem {
+ id: string;
+ title: string;
+ completed: boolean;
+}
+
+export interface TodoApi extends RestApi {
+ GET: {
+ '/todos': GetCollectionEndpoint;
+ '/todos/:id': GetEntityEndpoint;
+ };
+ POST: {
+ '/todos': PostEndpoint;
+ };
+ PATCH: {
+ '/todos/:id': PatchEndpoint;
+ };
+ DELETE: {
+ '/todos/:id': DeleteEndpoint;
+ };
+}
+```
+
+Each endpoint type spells out `body`, `url`, `query`, `headers`, and `result`. The shorthand types (`GetCollectionEndpoint`, `PatchEndpoint`, etc.) cover the standard CRUD shapes; for custom endpoints, declare your own type with the fields you need.
+
+## 2. Implement the server
+
+The endpoint generators take a `DataSetToken` directly — no need to pass `(Model, 'primaryKey')` tuples.
+
+```ts
+// service/src/index.ts
+import { createInjector } from '@furystack/inject';
+import {
+ Authenticate,
+ createDeleteEndpoint,
+ createGetCollectionEndpoint,
+ createGetEntityEndpoint,
+ createPatchEndpoint,
+ createPostEndpoint,
+ useHttpAuthentication,
+ useRestService,
+} from '@furystack/rest-service';
+import { TodoDataSet } from './todo-dataset.js';
+import type { TodoApi } from 'common';
+
+const injector = createInjector();
+useHttpAuthentication(injector);
+
+await useRestService({
+ injector,
+ port: 3000,
+ root: '/api',
+ cors: { credentials: true, origins: ['http://localhost:8080'] },
+ api: {
+ GET: {
+ '/todos': Authenticate()(createGetCollectionEndpoint(TodoDataSet)),
+ '/todos/:id': Authenticate()(createGetEntityEndpoint(TodoDataSet)),
+ },
+ POST: {
+ '/todos': Authenticate()(createPostEndpoint(TodoDataSet)),
+ },
+ PATCH: {
+ '/todos/:id': Authenticate()(createPatchEndpoint(TodoDataSet)),
+ },
+ DELETE: {
+ '/todos/:id': Authenticate()(createDeleteEndpoint(TodoDataSet)),
+ },
+ },
+});
+```
+
+Authorization, modifiers, and events configured on the DataSet apply automatically. `Authenticate()` short-circuits to a `401` for unauthenticated requests.
+
+### Custom endpoints
+
+When the CRUD generators don't fit, write the handler yourself. The handler receives a per-request `injector`, request/response objects, and helpers for `body`, `query`, `url`, and `headers`:
+
+```ts
+import { JsonResult, useRestService, HttpUserContext } from '@furystack/rest-service';
+
+await useRestService({
+ injector,
+ port: 3000,
+ root: '/api',
+ api: {
+ POST: {
+ '/todos/:id/complete': async ({ getUrlParams, injector }) => {
+ const { id } = getUrlParams();
+ const httpUser = injector.get(HttpUserContext);
+ const user = await httpUser.getCurrentUser();
+ const dataSet = injector.get(TodoDataSet);
+ await dataSet.update(injector, id, { completed: true, completedBy: user.username });
+ return JsonResult({ ok: true });
+ },
+ },
+ },
+});
+```
+
+The injector parameter is **scoped to the request** — disposable, isolated from sibling requests, and the right place to read `HttpUserContext`.
+
+## 3. Authentication
+
+`useHttpAuthentication` binds the `HttpAuthenticationSettings` and `HttpUserContext` tokens. Bind the throw-by-default `UserStore` and `SessionStore` to your persistent implementations first, and pass the user `DataSetToken`:
+
+```ts
+import { useHttpAuthentication, UserStore, SessionStore } from '@furystack/rest-service';
+import { AppUserStore, AppSessionStore, AppUserDataSet } from './stores.js';
+
+injector.bind(UserStore, ctx => ctx.inject(AppUserStore));
+injector.bind(SessionStore, ctx => ctx.inject(AppSessionStore));
+
+useHttpAuthentication(injector, {
+ cookieName: 'sessionId',
+ enableBasicAuth: true,
+ userDataSet: AppUserDataSet,
+});
+```
+
+For JWT-based auth, swap in `useJwtAuthentication` from `@furystack/auth-jwt`.
+
+## 4. Consume the API
+
+```ts
+// frontend/src/api-client.ts
+import { createClient } from '@furystack/rest-client-fetch';
+import type { TodoApi } from 'common';
+
+export const apiClient = createClient({
+ endpointUrl: 'http://localhost:3000/api',
+});
+
+const result = await apiClient({
+ method: 'GET',
+ action: '/todos',
+});
+// result.result is typed as TodoItem[]
+```
+
+IntelliSense walks you through `method` → `action` → required fields (`url`, `body`, `query`, `headers`) for the picked action, and the response type is inferred from the API interface.
+
+## What you get for free
+
+- **End-to-end type safety.** Change the API interface, recompile, every mismatch lights up.
+- **Telemetry.** `injector.get(ServerTelemetryToken).subscribe('onApiRequestError', ...)` to wire logging or alerting.
+- **Static files** via `useStaticFiles({ injector, baseUrl, path, port })`.
+- **HTTP and WebSocket proxying** via `useProxy({ injector, sourceBaseUrl, targetBaseUrl, ... })`.
+
+## Where to look next
+
+- [Data Validation](/getting-started/data-validation/) — runtime validation of request payloads against the API interface.
+- [Repository](/getting-started/repository/) — DataSets that back your endpoint generators.
diff --git a/src/content/posts/003-getting-started-with-inject.md b/src/content/posts/003-getting-started-with-inject.md
index 9b37806..b9e8b17 100644
--- a/src/content/posts/003-getting-started-with-inject.md
+++ b/src/content/posts/003-getting-started-with-inject.md
@@ -6,6 +6,8 @@ tags: ['Getting Started', 'tutorial', 'dependency-injection', 'inject']
image: img/003-getting-started-with-inject-cover.jpg
date: '2021-06-23T08:58:20.257Z'
draft: false
+deprecated: true
+supersededBy: /getting-started/inject/
excerpt: Dependency injection and Inversion of control is a common practice that tries to protect you from insanity that would happen when you realize that you can't refactor and test a giant global static app structure. @furystack/inject is a simple but powerful tool that you can use in NodeJs and in the browser.
---
diff --git a/src/content/posts/004-getting-started-with-data-stores.md b/src/content/posts/004-getting-started-with-data-stores.md
index 8251145..f8a0546 100644
--- a/src/content/posts/004-getting-started-with-data-stores.md
+++ b/src/content/posts/004-getting-started-with-data-stores.md
@@ -16,6 +16,8 @@ tags:
image: img/004-getting-started-with-data-stores-cover.jpg
date: '2021-06-23T09:58:20.257Z'
draft: false
+deprecated: true
+supersededBy: /getting-started/data-stores/
excerpt: Where should you store your data? SQL, NOSQL, InMemory or on a sticky note on the back of your pillow? Doesn't matter if you have a PhysicalStore implementation...
---
diff --git a/src/content/posts/005-getting-started-with-repository.md b/src/content/posts/005-getting-started-with-repository.md
index e1717ab..04810bb 100644
--- a/src/content/posts/005-getting-started-with-repository.md
+++ b/src/content/posts/005-getting-started-with-repository.md
@@ -6,6 +6,8 @@ tags: ['Getting Started', 'tutorial', 'data-storage', 'repository']
image: img/005-getting-started-with-repository-cover.jpg
date: '2021-06-23T10:58:20.257Z'
draft: false
+deprecated: true
+supersededBy: /getting-started/repository/
excerpt: A Repository is the next layer above the data stores. When setting up a repository, you can create DataSets that can rely on a previously configured physical store. The difference is that while PhysicalStore focuses on the data, DataSet focuses on business logic. You can authorize, check permissions, subscribe to entity changes, etc...
---
diff --git a/src/content/posts/006-getting-started-with-rest.md b/src/content/posts/006-getting-started-with-rest.md
index 6c41a24..e047ce2 100644
--- a/src/content/posts/006-getting-started-with-rest.md
+++ b/src/content/posts/006-getting-started-with-rest.md
@@ -15,6 +15,8 @@ tags:
image: img/006-getting-started-with-rest-cover.jpg
date: '2021-06-23T12:58:20.257Z'
draft: false
+deprecated: true
+supersededBy: /getting-started/rest/
excerpt: Designing and implementing APIs can be hard and consuming them can be frustrating, if they doesn't work as expected. REST API as a Typescript interface to the rescue!
---
diff --git a/src/content/posts/007-data-validation-with-rest-and-json-schema.md b/src/content/posts/007-data-validation-with-rest-and-json-schema.md
index 632c2dc..b8ddb6a 100644
--- a/src/content/posts/007-data-validation-with-rest-and-json-schema.md
+++ b/src/content/posts/007-data-validation-with-rest-and-json-schema.md
@@ -17,6 +17,8 @@ tags:
image: img/007-data-validation-cover.jpg
date: '2021-06-23T13:58:20.257Z'
draft: false
+deprecated: true
+supersededBy: /getting-started/data-validation/
excerpt: We have a strongly typed REST API interface with build-time type checking. Can we build runtime validation with a minimal effort? (Spoiler alert - yesss)
---
diff --git a/src/content/posts/010-showcase-app.md b/src/content/posts/010-showcase-app.md
index 3646100..374c552 100644
--- a/src/content/posts/010-showcase-app.md
+++ b/src/content/posts/010-showcase-app.md
@@ -21,6 +21,8 @@ excerpt: Updates on Shades - Kick-ass DataGrid updates, fragments and a brand ne
Using [fragments](https://reactjs.org/docs/fragments.html) in React is not a new concept so I've implemented this also in Shades. The concept is the same - you can avoid unneccessary DOM nesting with them. The syntax is also the same - you can check out some [common component code](https://github.com/furystack/furystack/blob/e6edd24c9a196f56ba5b3b2dd65f062c8d68cdd5/packages/shades-common-components/src/components/data-grid/body.tsx#L52) to get the idea.
+> ⚠️ **Outdated API.** The factory is now spelled `Shade(...)` (singular), and the option is `customElementName`. Fragments still work the same way. See the current [Shades source](https://github.com/furystack/furystack/tree/develop/packages/shades) for the up-to-date shape.
+
```tsx
export const HelloWorld = Shades({
shadowDomName: 'shades-hello-world',
diff --git a/src/content/posts/016-shades-theme-system.md b/src/content/posts/016-shades-theme-system.md
index d4212e1..d22bde5 100644
--- a/src/content/posts/016-shades-theme-system.md
+++ b/src/content/posts/016-shades-theme-system.md
@@ -127,6 +127,8 @@ Two things to notice here. First, the theme is `DeepPartial` — you can
The `ThemeProviderService` wraps this into an injectable singleton with event emission:
+> ⚠️ **Outdated API.** `ThemeProviderService` was declassed in the functional-DI rewrite and is now a plain-object factory behind a singleton token. The API surface (`setAssignedTheme`, `themeChanged` events, the `theme` accessor) is unchanged; only the declaration shape moved. See the [Dependency Injection](/getting-started/inject/) guide for the current pattern.
+
```typescript
@Injectable({ lifetime: 'singleton' })
export class ThemeProviderService extends EventHub<{ themeChanged: DeepPartial }> {
diff --git a/src/content/posts/017-cache-system.md b/src/content/posts/017-cache-system.md
index 396d00d..5f114c1 100644
--- a/src/content/posts/017-cache-system.md
+++ b/src/content/posts/017-cache-system.md
@@ -273,6 +273,8 @@ The capacity limit ensures you don't accidentally cache every user who's ever lo
Now let's flip to the frontend. You have a list of items, and clicking one opens a detail view that fetches full data:
+> ⚠️ **Outdated API.** The `@Injectable` decorator no longer exists in `@furystack/inject`. The current shape declares the service via `defineService({ name, lifetime: 'singleton', factory: ... })` and resolves it with `injector.get(...)`. The cache itself is unchanged. See the [Dependency Injection](/getting-started/inject/) guide for the current pattern.
+
```typescript
import { Cache } from '@furystack/cache';
import { Injectable } from '@furystack/inject';
diff --git a/src/content/posts/018-entity-sync.md b/src/content/posts/018-entity-sync.md
index 41bbcc4..d385e60 100644
--- a/src/content/posts/018-entity-sync.md
+++ b/src/content/posts/018-entity-sync.md
@@ -133,6 +133,8 @@ It's a discriminated union, so TypeScript narrows the type for you. Check the `s
Setting up entity sync on the server takes about as much code as you'd spend writing a TODO comment about how you should "add real-time updates later":
+> ⚠️ **Outdated API.** This snippet predates the functional-DI rewrite. The current shape uses `createInjector()`, `useWebSocketApi`, and `useEntitySync` with `DataSetToken`s — see the [REST](/getting-started/rest/) and [Repository](/getting-started/repository/) guides plus the [migration guide](https://github.com/furystack/furystack/blob/develop/docs/migrations/v7-functional-di.md) for the up-to-date entity-sync recipe.
+
```typescript
import { Injector } from '@furystack/inject';
import { useWebsockets } from '@furystack/websocket-api';
@@ -188,6 +190,8 @@ Each model registration accepts three optional settings:
On the client side, you create an `EntitySyncService`, register your models, and start subscribing:
+> ⚠️ **Outdated API.** Client setup now goes through `defineEntitySyncService(opts)` (mints a per-app token) and `injector.bind(...)`. See the [Dependency Injection](/getting-started/inject/) guide and the [migration guide](https://github.com/furystack/furystack/blob/develop/docs/migrations/v7-functional-di.md) for the current shape.
+
```typescript
import { EntitySyncService, createInMemoryCacheStore } from '@furystack/entity-sync-client';
diff --git a/src/content/posts/020-functional-di.md b/src/content/posts/020-functional-di.md
new file mode 100644
index 0000000..89900a0
--- /dev/null
+++ b/src/content/posts/020-functional-di.md
@@ -0,0 +1,324 @@
+---
+title: 'The Last Decorator'
+author: [gallayl]
+tags:
+ [
+ 'Architecture',
+ 'refactoring',
+ 'breaking-changes',
+ 'dependency-injection',
+ 'migration',
+ 'FuryStack',
+ 'inject',
+ 'core',
+ 'repository',
+ 'rest-service',
+ 'security',
+ 'logging',
+ 'websocket-api',
+ 'filesystem-store',
+ 'mongodb-store',
+ 'redis-store',
+ 'sequelize-store',
+ 'shades',
+ 'shades-common-components',
+ 'shades-showcase-app',
+ ]
+date: '2026-04-25T12:00:00.000Z'
+draft: false
+image: img/020-functional-di.jpg
+excerpt: "The latest FuryStack update throws @Injectable in the bin, replaces classes with tokens, and finally clears the runway for the next big TypeScript upgrade. Here's what changed and why your @Injected properties are about to get really lonely."
+---
+
+Almost four years ago, in [A little bit of Inject refactor](/posts/009-inject-refactor/), I wrote a slightly anxious sentence about decorator metadata being "doomed". I was being dramatic at the time. I was also, it turns out, _correct_. Then deeply, generationally, "the entire DI layer needs a redesign" correct.
+
+This is that redesign. Welcome to the latest FuryStack update — the release where `@Injectable` finally walks into the sea.
+
+## The decorator-shaped elephant
+
+Quick recap for newcomers: legacy TypeScript decorators (the ones FuryStack has been leaning on since 2018) live in TC39 limbo. The standards committee moved on to a stage-3 design with a completely different shape. The legacy variant only really worked because TypeScript's `emitDecoratorMetadata` flag stuffed type info into the compiled output at build time — a feature that [esbuild explicitly refuses to support](https://github.com/evanw/esbuild/issues/257), and that the wider tooling ecosystem has spent years quietly distancing itself from.
+
+I mitigated some of that in [A little bit of Inject refactor](/posts/009-inject-refactor/) by ditching constructor injection in favour of `@Injected` properties. That bought a few good years. But the writing was on the wall: the next TypeScript major was going to keep tightening the screws on the legacy decorator surface, the experimental flags were going to keep getting "experimental"-er, and any framework still leaning on `Reflect.metadata` was going to be left grinding through migration debt forever.
+
+So I did the boring grown-up thing. I ripped the decorators out, replaced them with a functional, token-based DI API, and gave the framework a clean runway to upgrade TypeScript without holding its breath.
+
+This post is the tour. It's also the polite warning: yes, this is a breaking change. Yes, every package got a major bump. Yes, there's a migration guide. I'll get to that.
+
+## Meet `defineService`
+
+The new mental model is short:
+
+> A service is a **factory** that returns an object. The factory is registered behind a **token**. The injector resolves tokens, caches by lifetime, and runs your `onDispose` callbacks on teardown.
+
+That's it. There are no classes. There is no `Reflect.metadata`. There is no constructor magic. Side-by-side:
+
+```typescript
+// before — decorators all the way down
+@Injectable({ lifetime: 'singleton' })
+class Counter {
+ private value = 0;
+ public increment(): number {
+ return ++this.value;
+ }
+}
+const svc = injector.getInstance(Counter);
+```
+
+```typescript
+// after — a factory and a token
+const Counter = defineService({
+ name: 'my-app/Counter',
+ lifetime: 'singleton',
+ factory: () => {
+ let value = 0;
+ return { increment: () => ++value };
+ },
+});
+const svc = injector.get(Counter);
+```
+
+The `Counter` identifier on the new side is doing double duty — it's both the token (for the injector) and the type (for consumers). That's the bit that makes the whole thing feel weirdly natural after a day or two: import a single thing, use it as a value (resolve it) or as a type (annotate variables), and the symbol identity is tracked by a fresh `Symbol` on every `defineService` call so accidental cross-package collisions are structurally impossible.
+
+Dependencies come from the factory's context:
+
+```typescript
+const PaymentService = defineService({
+ name: 'my-app/PaymentService',
+ lifetime: 'singleton',
+ factory: ({ inject, onDispose }) => {
+ const logger = inject(LoggerCollection);
+ const settings = inject(PaymentSettings);
+ const client = createPaymentClient(settings);
+ onDispose(async () => client.close());
+ return {
+ charge: async (amount: number) => {
+ logger.info({ message: `Charging ${amount}` });
+ return client.charge(amount);
+ },
+ };
+ },
+});
+```
+
+`onDispose` is a quietly important detail. Every factory gets one, and it runs in **reverse registration order** when the owning injector is disposed. No more "oh wait, did I forget to close that pool?" panic at process exit.
+
+For things that genuinely need `await` during construction, `defineServiceAsync` is the async cousin — same shape, the factory returns a `Promise`, consumers resolve via `injector.getAsync(Token)`. More on that later — it shows up again.
+
+## The injector's new vocabulary
+
+Most of the renames are mechanical. If the old API is in muscle memory, the new one will feel familiar within an hour:
+
+| Before | Now |
+| ----------------------------------------------- | -------------------------------------- |
+| `new Injector()` | `createInjector()` |
+| `injector.getInstance(Class)` | `injector.get(Token)` |
+| `injector.setExplicitInstance(instance, Class)` | `injector.bind(Token, () => instance)` |
+| `injector.createChild({ owner })` | `injector.createScope({ owner })` |
+| `injector.cachedSingletons.has(X)` | (gone — use a nullable scoped token) |
+
+A few additions worth knowing about:
+
+- **`getAsync(token)`** — resolves both sync and async tokens. The TypeScript signatures stop you from calling sync `get` on an async-only token, so the failure mode is a compile error, not a runtime surprise.
+- **`bind(token, factory)` + `invalidate(token)`** — `bind` overrides on the injector that owns the cached instance and drops any cached value. `invalidate` clears the cache without rebinding. Together they replace every "I want to swap this dependency for tests" pattern.
+- **`Symbol.asyncDispose`** — every injector implements it, so `await using injector = createInjector()` (or the existing `usingAsync` helper) cleans up the whole tree, recursively, in LIFO order.
+- **`createScope(opts?)`** — the renamed `createChild`. Scopes are cheap, disposable, and the right unit of isolation for per-request, per-message, or per-test work.
+
+## Stores and DataSets, now with metadata
+
+The old `addStore(injector, new Store({...}))` + `getRepository(injector).createDataSet(Model, 'pk', settings)` dance is gone. The new form has two functions doing the heavy lifting:
+
+```typescript
+const UserStore = defineStore({
+ name: 'my-app/UserStore',
+ model: User,
+ primaryKey: 'username',
+ factory: () => new InMemoryStore({ model: User, primaryKey: 'username' }),
+});
+
+const UserDataSet = defineDataSet({
+ name: 'my-app/UserDataSet',
+ store: UserStore,
+ settings: {
+ /* authorize, modifyOnAdd, etc. */
+ },
+});
+```
+
+`StoreToken` and `DataSetToken` both carry their model + primary-key metadata along for the ride, which means downstream APIs (REST endpoint generators, `SubscriptionManager.registerModel`, `getDataSetFor`) accept a single token argument instead of yet another `(Model, 'primaryKey')` tuple. One source of truth, type-checked at the declaration site, propagated everywhere it's needed.
+
+The backend store packages each ship a dedicated mint for their flavour:
+
+- `defineFileSystemStore({ name, model, primaryKey, fileName, tickMs? })`
+- `defineMongoDbStore({ name, model, primaryKey, url, db, collection })`
+- `defineRedisStore({ name, model, primaryKey, client })`
+- `defineSequelizeStore({ name, model, sequelizeModel, primaryKey, options })`
+
+Same idea, same shape. The MongoDB and Sequelize variants now expose their underlying client factories as singleton tokens with an internal connection pool that closes itself on injector dispose — so writing 80 tests against the same database no longer leaks 80 mongo clients into the void.
+
+### Throw-by-default stores
+
+Here's a small but opinionated change that pays off enormously in practice. Every persistent store that ships with FuryStack — `UserStore`, `SessionStore`, `RefreshTokenStore`, `PasswordCredentialStore`, `PasswordResetTokenStore` — is now a token whose default factory **throws**. As in: resolve it without binding it first, and you get a clear, named error telling you what's missing.
+
+```typescript
+// At app bootstrap:
+const AppUserStore = defineSequelizeStore({
+ name: 'my-app/AppUserStore',
+ model: User,
+ sequelizeModel: UserModel,
+ primaryKey: 'username',
+ options: { dialect: 'postgres' /* ... */ },
+});
+
+injector.bind(UserStore, ctx => ctx.inject(AppUserStore));
+```
+
+Tests opt into `InMemoryStore` per scope:
+
+```typescript
+injector.bind(UserStore, () => new InMemoryStore({ model: User, primaryKey: 'username' }));
+```
+
+This sounds annoying for about ninety seconds, until the first time you forget to wire up a database in a new app and the test suite tells you _exactly which store wasn't bound_ instead of mysteriously returning empty arrays. I have not missed the silent-empty-store debugging sessions. They are not invited back.
+
+## The Great Manager Massacre
+
+One of the quieter side effects of the rewrite was realising how many of the old "Manager" classes existed purely to paper over `@Injectable` ergonomics. Most of them turned out to be unnecessary the moment real DI tokens were on the table.
+
+The casualty list:
+
+- **`ServerManager`**, **`ApiManager`**, **`StaticServerManager`**, **`ProxyManager`** — all gone. Replaced by `HttpServerPoolToken` (a singleton-pooled HTTP server registry) and `ServerTelemetryToken` (an event hub emitting `onApiRequestError`, `onProxyFailed`, `onWebSocketActionFailed`, `onWebSocketProxyFailed`). The `useRestService` / `useStaticFiles` / `useProxy` helpers kept their public shapes; the internals are token-based and significantly less circular.
+- **`Repository`** — gone. `defineDataSet` covers everything `Repository.createDataSet` used to do, with stronger types and zero shared mutable state.
+- **`StoreManager`** — gone. Stores resolve through their tokens; `addStore` is no longer a thing.
+
+`HttpUserContext` got a small but important fix while the area was open. The old version cached the current user in a private instance field — which, combined with how scoped tokens cache on the first ancestor that resolves them, meant authenticated state could quietly leak across scopes when two parts of an app shared an `HttpUserContext` instance. The new version stores the cache in a `WeakMap>`, keyed by the per-request headers object. Same instance, but every request gets its own cache slot, and the WeakMap drops entries automatically when the request goes out of scope.
+
+The WebSocket layer got the most dramatic rewrite. `WebSocketAction` used to be a class with decorators, a constructor, and per-instance lifecycle. It's now a plain object:
+
+```typescript
+const PingAction: WebSocketAction = {
+ canExecute: ({ data }) => data.type === 'ping',
+ execute: async ({ injector, socket }) => {
+ const logger = injector.get(LoggerCollection);
+ logger.info({ message: 'pong' });
+ socket.send(JSON.stringify({ type: 'pong' }));
+ },
+};
+```
+
+Each incoming message gets a fresh `injector.createScope({ owner: message })`, the action runs inside it, and the scope is disposed in `finally` — same per-request isolation pattern the rest service has been using for years. Action failures route to `ServerTelemetryToken#onWebSocketActionFailed` instead of being absorbed by the action class itself. Multi-endpoint setups Just Work because `useWebSocketApi(...)` returns its own handle instead of registering a singleton.
+
+## `cachedSingletons.has(X)` is gone, long live nullable tokens
+
+Here's a niche-but-illuminating pattern. The old `
- import { Injector } from '@furystack/inject'
-import { createComponent, initializeShadeRoot } from '@furystack/shades'
+ import { createInjector } from '@furystack/inject'
+import { initializeShadeRoot } from '@furystack/shades'
import { TodoApp } from './components/todo-app.js'
-const shadeInjector = new Injector()
+const shadesInjector = createInjector()
const rootElement = document.getElementById('root') as HTMLDivElement
initializeShadeRoot({
- injector: shadeInjector,
+ injector: shadesInjector,
rootElement,
jsxElement: <TodoApp />,
})
frontend/src/components/todo-app.tsx
- A Shade component that uses @furystack/rest-client-fetch for type-safe API calls:
+ A Shade component using useObservable for reactive state and @furystack/rest-client-fetch for type-safe API calls:
import { createComponent, Shade } from '@furystack/shades'
import { createClient } from '@furystack/rest-client-fetch'
+import { ObservableValue } from '@furystack/utils'
import type { MyApi, TodoItem } from 'common'
+const client = createClient<MyApi>({
+ endpointUrl: 'http://localhost:3000/api',
+})
+
export const TodoApp = Shade({
customElementName: 'todo-app',
- render: ({ useState, element }) => {
- const client = createClient<MyApi>({
- endpointUrl: 'http://localhost:3000/api',
- })
+ render: ({ useObservable }) => {
+ const todos$ = new ObservableValue<TodoItem[]>([])
+ const input$ = new ObservableValue('')
- const [todos, setTodos] = useState<TodoItem[]>('todos', [])
- const [input, setInput] = useState('input', '')
+ const todos = useObservable('todos', todos$)
+ const input = useObservable('input', input$)
const loadTodos = async () => {
- const result = await client({
- method: 'GET',
- action: '/todos',
- })
- setTodos(result)
+ const result = await client({ method: 'GET', action: '/todos' })
+ todos$.setValue(result.result)
}
void loadTodos()
@@ -384,7 +394,7 @@ export const TodoApp = Shade({
action: '/todos',
body: { title: input },
})
- setInput('')
+ input$.setValue('')
void loadTodos()
}
@@ -395,7 +405,7 @@ export const TodoApp = Shade({
<input
value={input}
oninput={(e: Event) =>
- setInput((e.target as HTMLInputElement).value)}
+ input$.setValue((e.target as HTMLInputElement).value)}
placeholder="What needs to be done?"
/>
<button onclick={addTodo}>Add</button>
@@ -439,19 +449,29 @@ yarn start:frontend
You now have a working full-stack FuryStack application. From here you can:
-
- Swap
InMemoryStore for FileSystemStore or MongodbStore to persist data
+ Swap the InMemoryStore factory for defineFileSystemStore,
+ defineMongoDbStore, defineRedisStore, or
+ defineSequelizeStore to persist data — see Data Stores
-
Add authentication with
@furystack/auth-jwt and @furystack/security
- - Add authorization rules to your data sets via
@furystack/repository
-
- Explore the Getting Started hub for guided deep-dives into
- each package
+ Layer authorization, modifiers, and change events on your dataset — see Repository
+
+ -
+ Add runtime validation to your endpoints — see Data Validation
+
+ -
+ Explore the Getting Started hub for the full set of guides
diff --git a/src/pages/getting-started/index.astro b/src/pages/getting-started/index.astro
index 3ee61b1..b40cc4a 100644
--- a/src/pages/getting-started/index.astro
+++ b/src/pages/getting-started/index.astro
@@ -69,27 +69,30 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
Next steps
- Once you have a project running, dive deeper into individual FuryStack concepts:
+
+ Once you have a project running, dive deeper into individual FuryStack concepts. Each
+ guide below is kept up to date with the latest API.
+
-
- Dependency Injection
- — understand the
Injector and service lifecycle
+ Dependency Injection
+ — defineService, tokens, lifetimes, and disposable scopes
-
- Data Stores
- — in-memory, filesystem, MongoDB, and more
+ Data Stores
+ —
defineStore with in-memory, filesystem, MongoDB, Redis, or SQL backends
-
- Repository
- — data sets, authorization, and event hooks
+ Repository
+ — DataSets with authorization, modifiers, and change events
-
- REST
- — type-safe API definitions shared across the stack
+ REST
+ — type-safe API contracts shared between server and client
-
- Data Validation
- — JSON Schema validation for your endpoints
+ Data Validation
+ — JSON Schema validation for endpoint payloads
diff --git a/src/pages/packages.astro b/src/pages/packages.astro
index fb636d8..060a99b 100644
--- a/src/pages/packages.astro
+++ b/src/pages/packages.astro
@@ -1,5 +1,9 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+
+const allPosts = await getCollection('posts', ({ data }) => !data.draft);
+const usedTags = new Set(allPosts.flatMap(p => p.data.tags ?? []).map(t => t.toLowerCase()));
const packages = [
'@furystack/auth-google',
@@ -42,6 +46,7 @@ const packages = [
{
packages.map(pkg => {
const shortName = pkg.split('/')[1];
+ const hasPosts = usedTags.has(shortName.toLowerCase());
return (
);
})
diff --git a/yarn.lock b/yarn.lock
index a3d0317..9e7116c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -35,12 +35,12 @@ __metadata:
languageName: node
linkType: hard
-"@astrojs/internal-helpers@npm:0.8.0":
- version: 0.8.0
- resolution: "@astrojs/internal-helpers@npm:0.8.0"
+"@astrojs/internal-helpers@npm:0.9.0":
+ version: 0.9.0
+ resolution: "@astrojs/internal-helpers@npm:0.9.0"
dependencies:
- picomatch: "npm:^4.0.3"
- checksum: 10c0/1befdd71b295e028d0a940cc7d72bb75d574c7543497836fb4daaa6332fb6187cb99196d49be5ca605cc75cb9f8b87c8eeb08543ca99b0aead8295fbf3aa1245
+ picomatch: "npm:^4.0.4"
+ checksum: 10c0/6513b28955d1d4fe250c2978e0496d1a5b72be0a19db427e475bc31930d1704cdea63f67862eefd046d480aa7f1fb775b14e143e66ef70eabf187f17655d0047
languageName: node
linkType: hard
@@ -80,11 +80,11 @@ __metadata:
languageName: node
linkType: hard
-"@astrojs/markdown-remark@npm:7.1.0":
- version: 7.1.0
- resolution: "@astrojs/markdown-remark@npm:7.1.0"
+"@astrojs/markdown-remark@npm:7.1.1":
+ version: 7.1.1
+ resolution: "@astrojs/markdown-remark@npm:7.1.1"
dependencies:
- "@astrojs/internal-helpers": "npm:0.8.0"
+ "@astrojs/internal-helpers": "npm:0.9.0"
"@astrojs/prism": "npm:4.0.1"
github-slugger: "npm:^2.0.0"
hast-util-from-html: "npm:^2.0.3"
@@ -105,7 +105,7 @@ __metadata:
unist-util-visit: "npm:^5.1.0"
unist-util-visit-parents: "npm:^6.0.2"
vfile: "npm:^6.0.3"
- checksum: 10c0/8c27865a3d522326731d81a23d98fa714a73b81b384d482bfcb946e9fa07f80d04a9b22b210d2dfbcfc605a559d4fc9eb13522be6b8ffa077151a4c704d0516f
+ checksum: 10c0/f64f8900008aa9b2ba0d574c1f2a34a9cf82192c28d3b19a8b157738bbff721424b50d16aa0d67e3d98cbfc1581c3b3b6f0029d66d449d0ebba2c7c7612afc5c
languageName: node
linkType: hard
@@ -1291,56 +1291,66 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/eslint-plugin@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/eslint-plugin@npm:8.58.2"
+"@typescript-eslint/eslint-plugin@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/eslint-plugin@npm:8.59.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
- "@typescript-eslint/scope-manager": "npm:8.58.2"
- "@typescript-eslint/type-utils": "npm:8.58.2"
- "@typescript-eslint/utils": "npm:8.58.2"
- "@typescript-eslint/visitor-keys": "npm:8.58.2"
+ "@typescript-eslint/scope-manager": "npm:8.59.0"
+ "@typescript-eslint/type-utils": "npm:8.59.0"
+ "@typescript-eslint/utils": "npm:8.59.0"
+ "@typescript-eslint/visitor-keys": "npm:8.59.0"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
- "@typescript-eslint/parser": ^8.58.2
+ "@typescript-eslint/parser": ^8.59.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/87dd29c7a87461c586e3025cde2a6e35c7cc99e69c3a93ee8254f1523ab6d4d5d322cacd476e42a3aa87581fbcf9039ef528a638a80a5c9beb1c5ebb4cc557e2
+ checksum: 10c0/f98171ecad6a5106fe978df155f4b65a72dfdadfcd663651b633b61480b543e74796baa224a1393e323f9514901604fe6302323c4b80b79f7a98512a01bc6461
languageName: node
linkType: hard
-"@typescript-eslint/parser@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/parser@npm:8.58.2"
+"@typescript-eslint/parser@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/parser@npm:8.59.0"
dependencies:
- "@typescript-eslint/scope-manager": "npm:8.58.2"
- "@typescript-eslint/types": "npm:8.58.2"
- "@typescript-eslint/typescript-estree": "npm:8.58.2"
- "@typescript-eslint/visitor-keys": "npm:8.58.2"
+ "@typescript-eslint/scope-manager": "npm:8.59.0"
+ "@typescript-eslint/types": "npm:8.59.0"
+ "@typescript-eslint/typescript-estree": "npm:8.59.0"
+ "@typescript-eslint/visitor-keys": "npm:8.59.0"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/7ce3e5086b5376a91f2932fda6e0d6777ff457535eff9c133852b21c895dc56933dcda173430352850e77c2437f81c5699fac9c70207abbbd087882766b88758
+ checksum: 10c0/996a7b43f8a515ebbd06455c9f53065c561c8519bc4f634d6783b92832aa69e47945478d1601a87582f9f7b303becc172d5d7f776e201b2a2d375bc762ad4015
languageName: node
linkType: hard
-"@typescript-eslint/project-service@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/project-service@npm:8.58.2"
+"@typescript-eslint/project-service@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/project-service@npm:8.59.0"
dependencies:
- "@typescript-eslint/tsconfig-utils": "npm:^8.58.2"
- "@typescript-eslint/types": "npm:^8.58.2"
+ "@typescript-eslint/tsconfig-utils": "npm:^8.59.0"
+ "@typescript-eslint/types": "npm:^8.59.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/57fa2a54452f9d9058781feb8d99d7a25096d55db15783a552b242d144992ccf893548672d3bc554c1bc0768cd8c80dbb467e9aff0db471ebcc876d4409cf75e
+ checksum: 10c0/ffba9595a427235bbeb0e5c7db3486f8d01dd8f8686964b4f82084e82008c49b897d01c4d331f33a9ce29edae70a9286f6fdedec4bf9037d732d9c9e86ebc7ea
+ languageName: node
+ linkType: hard
+
+"@typescript-eslint/scope-manager@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/scope-manager@npm:8.59.0"
+ dependencies:
+ "@typescript-eslint/types": "npm:8.59.0"
+ "@typescript-eslint/visitor-keys": "npm:8.59.0"
+ checksum: 10c0/d372f08be190d01e6d237932dc0d77808a9dc0a34fe8f690a3eac496d6e2f93c030c6ccb5000b35e825a6cfc4d9ca69a00f2ccda334115a9865a9d02cd603e52
languageName: node
linkType: hard
-"@typescript-eslint/scope-manager@npm:8.58.2, @typescript-eslint/scope-manager@npm:^7.0.0 || ^8.0.0":
+"@typescript-eslint/scope-manager@npm:^7.0.0 || ^8.0.0":
version: 8.58.2
resolution: "@typescript-eslint/scope-manager@npm:8.58.2"
dependencies:
@@ -1350,46 +1360,53 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/tsconfig-utils@npm:8.58.2, @typescript-eslint/tsconfig-utils@npm:^8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.2"
+"@typescript-eslint/tsconfig-utils@npm:8.59.0, @typescript-eslint/tsconfig-utils@npm:^8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/tsconfig-utils@npm:8.59.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/d3dc874ab43af39245ee8383bb6d39c985e64c43b81a7bbf18b7982047473366c252e19a9fbfe38df30c677b42133aa43a1c0a75e92b8de5d2e64defd4b3a05e
+ checksum: 10c0/ab482c22f23774d24b3048c9fcdc5e0b94137064b3af901f4b0327da2270c2b2961c19165ccf8bdeaedfa83138be98c5cd8edcdc89deb6187baf6438cd8584b0
languageName: node
linkType: hard
-"@typescript-eslint/type-utils@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/type-utils@npm:8.58.2"
+"@typescript-eslint/type-utils@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/type-utils@npm:8.59.0"
dependencies:
- "@typescript-eslint/types": "npm:8.58.2"
- "@typescript-eslint/typescript-estree": "npm:8.58.2"
- "@typescript-eslint/utils": "npm:8.58.2"
+ "@typescript-eslint/types": "npm:8.59.0"
+ "@typescript-eslint/typescript-estree": "npm:8.59.0"
+ "@typescript-eslint/utils": "npm:8.59.0"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/1e7248694c15b5e78aeb573aef755513910f6a7ec1842223ec0c8429b6abd7342996de215aefab78520e64d2e8600c9829bdf56132476cb86703fd54f2492467
+ checksum: 10c0/e2f2176a9bce81c19b53accf4e9189c60b1b84717cf129a6d003a2271019e30d410d2ccdc0fc6a37cbb8274a1b297d7d30a116189110f9d24a86391ee24a9fef
languageName: node
linkType: hard
-"@typescript-eslint/types@npm:8.58.2, @typescript-eslint/types@npm:^7.0.0 || ^8.0.0, @typescript-eslint/types@npm:^7.7.1 || ^8, @typescript-eslint/types@npm:^8.58.2":
+"@typescript-eslint/types@npm:8.58.2, @typescript-eslint/types@npm:^7.0.0 || ^8.0.0, @typescript-eslint/types@npm:^7.7.1 || ^8":
version: 8.58.2
resolution: "@typescript-eslint/types@npm:8.58.2"
checksum: 10c0/6707c1a2ec921b9ae441b35d9cb4e0af11673a67e332a366e3033f1d558ff5db4f39021872c207fb361841670e9ffcc4981f19eb21e4495a3a031d02015637a7
languageName: node
linkType: hard
-"@typescript-eslint/typescript-estree@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/typescript-estree@npm:8.58.2"
+"@typescript-eslint/types@npm:8.59.0, @typescript-eslint/types@npm:^8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/types@npm:8.59.0"
+ checksum: 10c0/2750b1e21290dffe90a424fe05c2bab701f60a7b51b5e0921ed14bb1a5fc29ff3fe8f286817d2287e93ff78e33e6626f6ce26d0bc79a729bd608deda77a9bdde
+ languageName: node
+ linkType: hard
+
+"@typescript-eslint/typescript-estree@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/typescript-estree@npm:8.59.0"
dependencies:
- "@typescript-eslint/project-service": "npm:8.58.2"
- "@typescript-eslint/tsconfig-utils": "npm:8.58.2"
- "@typescript-eslint/types": "npm:8.58.2"
- "@typescript-eslint/visitor-keys": "npm:8.58.2"
+ "@typescript-eslint/project-service": "npm:8.59.0"
+ "@typescript-eslint/tsconfig-utils": "npm:8.59.0"
+ "@typescript-eslint/types": "npm:8.59.0"
+ "@typescript-eslint/visitor-keys": "npm:8.59.0"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
@@ -1397,22 +1414,22 @@ __metadata:
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/60a323f60eff9b4bb6eb3121c5f6292e7962517a329a8a9f828e8f07516de78e6a7c1b1b1cfd732f39edf184fe57828ca557fbc63b74c61b54bcb679a69e249c
+ checksum: 10c0/82d3dfb4de591d9a39d2c4dafc13f14b4940f5b116fb3db311935137aa7e34c9dce3209aaeace118070847b2355df7c185ff1e0f2a36232c3aea9b5fa2652f98
languageName: node
linkType: hard
-"@typescript-eslint/utils@npm:8.58.2":
- version: 8.58.2
- resolution: "@typescript-eslint/utils@npm:8.58.2"
+"@typescript-eslint/utils@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/utils@npm:8.59.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
- "@typescript-eslint/scope-manager": "npm:8.58.2"
- "@typescript-eslint/types": "npm:8.58.2"
- "@typescript-eslint/typescript-estree": "npm:8.58.2"
+ "@typescript-eslint/scope-manager": "npm:8.59.0"
+ "@typescript-eslint/types": "npm:8.59.0"
+ "@typescript-eslint/typescript-estree": "npm:8.59.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/d83e6c7c1b01236d255cabe2a5dc5384eedebc9f9af6aa19cc2ab7d8b280f86912f2b1a87659b2754919afd2606820b4e53862ac91970794e2980bc97487537c
+ checksum: 10c0/eca4e5a18ae8e8c4360b05758fa142465daef3a9dffe4d78b15607b4680698eece96f899bce1e8d83427da74ddfbca80a95456727b8b9239816528978180b047
languageName: node
linkType: hard
@@ -1426,6 +1443,16 @@ __metadata:
languageName: node
linkType: hard
+"@typescript-eslint/visitor-keys@npm:8.59.0":
+ version: 8.59.0
+ resolution: "@typescript-eslint/visitor-keys@npm:8.59.0"
+ dependencies:
+ "@typescript-eslint/types": "npm:8.59.0"
+ eslint-visitor-keys: "npm:^5.0.0"
+ checksum: 10c0/09ec24c9c9d0a3ccb57bb2ab3dfd8deca124339aba6621503285c22765a4dfc89bf3d31e337dd647b1cdf89bac384e3a62e0f5b8c1d5a93d16d1f417144e3226
+ languageName: node
+ linkType: hard
+
"@ungap/structured-clone@npm:^1.0.0":
version: 1.3.0
resolution: "@ungap/structured-clone@npm:1.3.0"
@@ -1689,13 +1716,13 @@ __metadata:
languageName: node
linkType: hard
-"astro@npm:^6.1.8":
- version: 6.1.8
- resolution: "astro@npm:6.1.8"
+"astro@npm:^6.1.9":
+ version: 6.1.9
+ resolution: "astro@npm:6.1.9"
dependencies:
"@astrojs/compiler": "npm:^3.0.1"
- "@astrojs/internal-helpers": "npm:0.8.0"
- "@astrojs/markdown-remark": "npm:7.1.0"
+ "@astrojs/internal-helpers": "npm:0.9.0"
+ "@astrojs/markdown-remark": "npm:7.1.1"
"@astrojs/telemetry": "npm:3.3.1"
"@capsizecss/unpack": "npm:^4.0.0"
"@clack/prompts": "npm:^1.1.0"
@@ -1727,7 +1754,7 @@ __metadata:
p-queue: "npm:^9.1.0"
package-manager-detector: "npm:^1.6.0"
piccolore: "npm:^0.1.3"
- picomatch: "npm:^4.0.3"
+ picomatch: "npm:^4.0.4"
rehype: "npm:^13.0.2"
semver: "npm:^7.7.4"
sharp: "npm:^0.34.0"
@@ -1741,9 +1768,9 @@ __metadata:
ultrahtml: "npm:^1.6.0"
unifont: "npm:~0.7.4"
unist-util-visit: "npm:^5.1.0"
- unstorage: "npm:^1.17.4"
+ unstorage: "npm:^1.17.5"
vfile: "npm:^6.0.3"
- vite: "npm:^7.3.1"
+ vite: "npm:^7.3.2"
vitefu: "npm:^1.1.2"
xxhash-wasm: "npm:^1.1.0"
yargs-parser: "npm:^22.0.0"
@@ -1753,7 +1780,7 @@ __metadata:
optional: true
bin:
astro: bin/astro.mjs
- checksum: 10c0/a6e35143622a3dff2e5c432d2860f760b00c770e61912081e46132c8c46c80f0d83cbaafca74f3225e3dc501dec6c95ea50ac9a822b24d05d8f15c3f70dfd42f
+ checksum: 10c0/bacb7dab6a0b0c5727a7f3247b8527e7043756bbfda5103e9efb60cef697be4c8d26185d4d3a5ab8cb8415ff4a8b5ab4cff0b2eab43f0cc9aac20389ba667dda
languageName: node
linkType: hard
@@ -2802,7 +2829,7 @@ __metadata:
"@astrojs/sitemap": "npm:^3.7.2"
"@eslint/js": "npm:^10.0.1"
"@types/node": "npm:^25.6.0"
- astro: "npm:^6.1.8"
+ astro: "npm:^6.1.9"
date-fns: "npm:^4.1.0"
eslint: "npm:^10.2.1"
eslint-plugin-astro: "npm:^1.7.0"
@@ -2813,7 +2840,7 @@ __metadata:
remark-smartypants: "npm:^3.0.2"
sharp: "npm:^0.34.5"
typescript: "npm:^6.0.3"
- typescript-eslint: "npm:^8.58.2"
+ typescript-eslint: "npm:^8.59.0"
languageName: unknown
linkType: soft
@@ -5309,18 +5336,18 @@ __metadata:
languageName: node
linkType: hard
-"typescript-eslint@npm:^8.58.2":
- version: 8.58.2
- resolution: "typescript-eslint@npm:8.58.2"
+"typescript-eslint@npm:^8.59.0":
+ version: 8.59.0
+ resolution: "typescript-eslint@npm:8.59.0"
dependencies:
- "@typescript-eslint/eslint-plugin": "npm:8.58.2"
- "@typescript-eslint/parser": "npm:8.58.2"
- "@typescript-eslint/typescript-estree": "npm:8.58.2"
- "@typescript-eslint/utils": "npm:8.58.2"
+ "@typescript-eslint/eslint-plugin": "npm:8.59.0"
+ "@typescript-eslint/parser": "npm:8.59.0"
+ "@typescript-eslint/typescript-estree": "npm:8.59.0"
+ "@typescript-eslint/utils": "npm:8.59.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10c0/6065fe90674e89100b3192716fc641d80de4b586fe244c00e2c97d47923166ab3286f895685bf9570919c8606724f1196486f09e7841ca73bdf05d5df0752945
+ checksum: 10c0/b14b4bf6878e9745d92c0bc2b3c68ea29e8e524037a10e05873ad58b0dd1961313c05f406273b99c4128fd49bde2d9b3233bcec636896e9a70ed8167a3d0a9c5
languageName: node
linkType: hard
@@ -5492,7 +5519,7 @@ __metadata:
languageName: node
linkType: hard
-"unstorage@npm:^1.17.4":
+"unstorage@npm:^1.17.5":
version: 1.17.5
resolution: "unstorage@npm:1.17.5"
dependencies:
@@ -5613,7 +5640,7 @@ __metadata:
languageName: node
linkType: hard
-"vite@npm:^7.3.1":
+"vite@npm:^7.3.2":
version: 7.3.2
resolution: "vite@npm:7.3.2"
dependencies: