mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a90b9356b | |||
| b070fb591e | |||
| 6489d08434 | |||
| 81bf41ce34 | |||
| d9ab583a16 | |||
| e68df5e606 | |||
| 6304ef81f6 | |||
| 6c7e82781d | |||
| a8e5c45a22 | |||
| ee28c2ef0c | |||
| 9219bf3b3c | |||
| e437ce7120 | |||
| 2417833ed7 | |||
| ab1eddcb6a | |||
| 237b42bb9c | |||
| d206c38d87 | |||
| d160af5cec | |||
| d03cd7f7bc | |||
| 4afba215b9 | |||
| dc8aca6a97 | |||
| 4121597fa2 | |||
| 9628720f87 | |||
| ff216ec4e5 | |||
| 06f9c3ee15 | |||
| 7751ee5db8 | |||
| feb3911d46 | |||
| ab545d3ca2 | |||
| 06c3a6f89b | |||
| 898a825f6e | |||
| a5ddf6aa93 | |||
| 5aa286298c | |||
| 55cb2bacef | |||
| e6ea3f64d7 | |||
| a5f5ea2084 | |||
| 6fb8e9a0e7 | |||
| abc6de6ad3 | |||
| 71b9a0f546 | |||
| 038ec0d672 | |||
| 7832328a50 | |||
| e5bfc602e1 | |||
| 0318e595f2 | |||
| 80b0716a4a | |||
| 3894271b17 | |||
| 8bf8b22e57 | |||
| 367d5878df | |||
| fb429e57e2 | |||
| 9008f9c6a3 | |||
| 6321a9935a | |||
| eb22289460 | |||
| c3198394de | |||
| 51237f5e1a | |||
| 01483b912f | |||
| 6288faef33 | |||
| 384e337bbd | |||
| cc1e1976f7 | |||
| 224d98879a | |||
| d1522c97ea | |||
| 8805f017a1 | |||
| 0ef06375c0 | |||
| 3d5c2cd3a8 | |||
| fc60dfd042 | |||
| c8ee30b3da | |||
| b85c3a8a9b | |||
| e20c2ef92c | |||
| 5873baad3c | |||
| aca162f9c8 | |||
| 9f0997c28a | |||
| 1c114d9533 | |||
| 68063986d1 | |||
| 53f8609e9d | |||
| 833bfa5049 | |||
| defcdfef0b | |||
| 348ac7f8b0 | |||
| 16b1cde8ea | |||
| 436b195242 | |||
| ccd504440e | |||
| 023f5391e4 | |||
| 11e3488917 | |||
| 9685c5d4e1 | |||
| 9b008be512 | |||
| 5352331464 | |||
| d9bce049ef | |||
| 0e4f25cf11 | |||
| 4a5bdade8d | |||
| 047bb80a97 | |||
| d826cb23a3 | |||
| 23a9e7c278 | |||
| a9fe43130e | |||
| 3b7ec06703 | |||
| 55a124b519 | |||
| dc3fe330d7 | |||
| d541167fb3 | |||
| 7e8b357bbb | |||
| 521d8bc6ff | |||
| 19f1941544 | |||
| f9373bd0e6 | |||
| 5384738b67 | |||
| d82e28e6b1 | |||
| c38bc95b1d | |||
| 6651fce099 | |||
| 91a1fc8a0c | |||
| d00e5fe68c | |||
| 6faf0d421f | |||
| 19e51fae63 | |||
| 7e01cc6101 | |||
| 103cbd11df | |||
| 03d3b8e407 | |||
| a97a79f11e | |||
| 90b4de7093 | |||
| 81f3914e7f | |||
| 5163e2142e | |||
| 15ed075de3 | |||
| 09b9a5e857 | |||
| 4f7925dfd8 | |||
| 21e970cd8c | |||
| 40b70e7a20 | |||
| 9958dd7308 | |||
| fef77eea61 | |||
| ed41887d74 | |||
| 8ba4793160 | |||
| 65e17db2a2 | |||
| e21b5e5b55 | |||
| 2e6b95e484 | |||
| 4e31f48917 | |||
| ac891a4b61 | |||
| 412eab6653 | |||
| 2fb07698d5 | |||
| 64e400c4f1 | |||
| 86d6e891a8 | |||
| 7b86611f35 | |||
| 9e316a92f6 | |||
| 12ef72d595 | |||
| 75c29ae5e2 | |||
| f0551f50d1 | |||
| 03aac3437b | |||
| f07f3d1007 | |||
| c833b2b539 | |||
| 6070394cca |
+267
@@ -0,0 +1,267 @@
|
||||
# Contributing to bknd
|
||||
|
||||
Thank you for your interest in contributing to bknd. This guide will help you get started, understand the codebase, and submit contributions that align with the project's direction.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Before You Start](#before-you-start)
|
||||
- [Important Resources](#important-resources)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Understanding the Codebase](#understanding-the-codebase)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Code Style](#code-style)
|
||||
- [Submitting Changes](#submitting-changes)
|
||||
- [AI-Generated Code](#ai-generated-code)
|
||||
- [Contributing to Documentation](#contributing-to-documentation)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Getting Help](#getting-help)
|
||||
- [Contributors](#contributors)
|
||||
- [Maintainers](#maintainers)
|
||||
|
||||
## Before You Start
|
||||
|
||||
**Open a GitHub Issue before writing code.** This is the preferred way to propose any change. It lets maintainers and the community align on the approach before time is spent on implementation. If you have discussed a contribution in the [Discord server](https://discord.com/invite/952SFk8Tb8) instead, include a link to the relevant Discord thread in your pull request. Pull requests submitted without a corresponding issue or Discord discussion may be closed.
|
||||
|
||||
**Unsolicited architectural changes will be closed.** The internal architecture of bknd is intentional. Refactors, restructuring, or changes to core patterns must be discussed and approved before any code is written.
|
||||
|
||||
Contributions that are generally welcome (after opening an issue):
|
||||
|
||||
- Bug fixes
|
||||
- New adapters, strategies, or storage backends
|
||||
- Documentation improvements
|
||||
- New examples
|
||||
- Test coverage improvements
|
||||
|
||||
**A note on versioning**: bknd is pre-1.0 and under active development. Full backward compatibility is not guaranteed before v1.0.0. Contributors should be aware that APIs and internal interfaces may change between releases.
|
||||
|
||||
## Important Resources
|
||||
|
||||
- **Documentation**: https://docs.bknd.io
|
||||
- **Issue Tracker**: https://github.com/bknd-io/bknd/issues
|
||||
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22.13 or higher
|
||||
- Bun 1.3.3
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Fork the repository and clone your fork.
|
||||
|
||||
2. Install dependencies from the repo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Navigate to the main app package:
|
||||
|
||||
```bash
|
||||
cd app
|
||||
```
|
||||
|
||||
4. Copy the example environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
The default `.env.example` includes a file-based SQLite database (`file:.db/dev.db`). You can change `VITE_DB_URL` to `:memory:` for an in-memory database instead.
|
||||
|
||||
5. Start the dev server:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
The dev server runs on `http://localhost:28623` using Vite with Hono.
|
||||
|
||||
## Project Structure
|
||||
|
||||
This is a Bun monorepo using workspaces. The vast majority of the code lives in `app/`.
|
||||
|
||||
```
|
||||
bknd/
|
||||
app/ # Main "bknd" npm package (this is where most work happens)
|
||||
src/
|
||||
adapter/ # Runtime/framework adapters (node, bun, cloudflare, nextjs, astro, etc.)
|
||||
auth/ # Authentication module (strategies, sessions, user pool)
|
||||
cli/ # CLI commands
|
||||
core/ # Shared utilities, event system, drivers, server helpers
|
||||
data/ # Data module (entities, fields, relations, queries, schema)
|
||||
flows/ # Workflow engine and tasks
|
||||
media/ # Media module (storage adapters, uploads)
|
||||
modes/ # Configuration modes (code, hybrid, db)
|
||||
modules/ # Module system, MCP server, permissions framework
|
||||
plugins/ # Built-in plugins (auth, data, cloudflare, dev)
|
||||
ui/ # All frontend code
|
||||
client/ # TypeScript SDK and React hooks (exported as bknd/client)
|
||||
elements/ # React components for auth/media (exported as bknd/elements)
|
||||
(everything else) # Admin UI (exported as bknd/ui)
|
||||
App.ts # Central application orchestrator
|
||||
index.ts # Main package exports
|
||||
__test__/ # Unit tests (mirrors src/ structure)
|
||||
e2e/ # End-to-end tests (Playwright)
|
||||
build.ts # Build script (tsup/esbuild)
|
||||
build.cli.ts # CLI build script
|
||||
packages/ # Small satellite packages
|
||||
cli/ # Standalone CLI package (bknd-cli)
|
||||
plasmic/ # Plasmic integration
|
||||
postgres/ # Postgres helper
|
||||
sqlocal/ # SQLocal helper
|
||||
docs/ # Documentation site (Next.js + Fumadocs, deployed to docs.bknd.io)
|
||||
examples/ # Example projects across runtimes and frameworks
|
||||
docker/ # Docker configuration
|
||||
```
|
||||
|
||||
## Understanding the Codebase
|
||||
|
||||
### Path Aliases
|
||||
|
||||
The project uses TypeScript path aliases defined in `app/tsconfig.json`. Imports like `core/utils`, `data/connection`, or `auth/authenticate` resolve to directories under `app/src/`. When reading source code, keep this in mind -- an import like `import { Connection } from "data/connection"` refers to `app/src/data/connection`, not an external package.
|
||||
|
||||
### Module System
|
||||
|
||||
bknd is built around four core modules, each with its own schema, API routes, and permissions:
|
||||
|
||||
- **Data** -- entity definitions, field types, relations, queries (backed by Kysely)
|
||||
- **Auth** -- authentication strategies, sessions, user management
|
||||
- **Media** -- file storage with pluggable adapters (S3, Cloudinary, local filesystem, etc.)
|
||||
- **Flows** -- workflow automation and task execution
|
||||
|
||||
These modules are managed by the `ModuleManager` (in `app/src/modules/`), which handles configuration, building, and lifecycle.
|
||||
|
||||
### Adapter Pattern
|
||||
|
||||
Adapters in `app/src/adapter/` allow bknd to run on different runtimes and frameworks. Each adapter provides the glue between bknd's Hono-based server and a specific environment (Node, Bun, Cloudflare Workers, Next.js, Astro, etc.).
|
||||
|
||||
### Plugin System
|
||||
|
||||
Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `beforeBuild`, `onBuilt`, `onServerInit`, and `schema`. They can add entities, register routes, and extend behavior.
|
||||
|
||||
### Build System
|
||||
|
||||
The build is handled by a custom `app/build.ts` script using tsup (esbuild under the hood). It builds four targets in parallel:
|
||||
|
||||
- **API** -- the core backend
|
||||
- **UI** -- the admin interface
|
||||
- **Elements** -- standalone React components
|
||||
- **Adapters** -- all runtime/framework adapters
|
||||
|
||||
## Running Tests
|
||||
|
||||
All test commands run from the `app/` directory.
|
||||
|
||||
| Command | Runner | What it runs |
|
||||
|---|---|---|
|
||||
| `bun run test` | Bun test | Unit tests (`*.spec.ts` files) |
|
||||
| `bun run test:node` | Vitest | Node-specific tests (`*.vi-test.ts`, `*.vitest.ts`) |
|
||||
| `bun run test:e2e` | Playwright | End-to-end tests (`*.e2e-spec.ts` in `e2e/`) |
|
||||
| `bun run types` | TypeScript | Type checking (no emit) |
|
||||
|
||||
CI runs both Bun and Node tests, with a Postgres 17 service for database integration tests.
|
||||
|
||||
## Code Style
|
||||
|
||||
The project uses **Biome** for formatting and linting.
|
||||
|
||||
- 3-space indentation for JavaScript/TypeScript
|
||||
- 100-character line width
|
||||
- Spaces (not tabs)
|
||||
|
||||
To format and lint, run from the repo root:
|
||||
|
||||
```bash
|
||||
bunx biome format --write ./app
|
||||
bunx biome lint --changed --write ./app
|
||||
```
|
||||
|
||||
Run both before submitting a PR. CI will catch style issues, but it is better to fix them locally first.
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
1. Open a GitHub Issue describing your proposed change, or link to a Discord thread where it was discussed. Wait for feedback before writing code.
|
||||
2. Fork the repo and create a branch from `main`.
|
||||
3. Keep changes focused and minimal. One PR per issue.
|
||||
4. Add or update tests if applicable.
|
||||
5. Run the test suite and linter before pushing.
|
||||
6. Open a pull request against `main`. Reference the issue number in the PR description.
|
||||
|
||||
Expect a response from maintainers, but be patient -- this is an actively developed project and review may take some time.
|
||||
|
||||
## AI-Generated Code
|
||||
|
||||
If you use AI tools to write or assist with your code, you must:
|
||||
|
||||
- **Fully review all AI-generated code yourself** before submitting. You are responsible for understanding and standing behind every line in your PR.
|
||||
- **Include the prompts used** in the PR description. This gives maintainers additional context for reviewing the code.
|
||||
|
||||
Pull requests with unreviewed AI-generated code will be closed.
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
The documentation site lives in the `docs/` directory and is a **separate Next.js application** built with [Fumadocs](https://fumadocs.dev). It uses **npm** (not Bun) as its package manager.
|
||||
|
||||
### Docs Setup
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The docs dev server runs on `http://localhost:3000`.
|
||||
|
||||
### Where Documentation Lives
|
||||
|
||||
Documentation content is written in MDX and located in `docs/content/docs/`. The directory is organized into:
|
||||
|
||||
- `(documentation)/` -- core documentation pages
|
||||
- `guide/` -- guides and tutorials
|
||||
- `api-reference/` -- API reference (partially auto-generated from OpenAPI)
|
||||
|
||||
To add or edit a page, create or modify an `.mdx` file in the appropriate directory. Page metadata is defined via frontmatter at the top of each file. Navigation order is controlled by `meta.json` files in each directory.
|
||||
|
||||
### Building the Docs
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This generates the OpenAPI and MCP reference pages before building the Next.js site. Make sure the build succeeds locally before submitting a docs PR.
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Open a GitHub Issue with:
|
||||
|
||||
- A clear title describing the problem.
|
||||
- Steps to reproduce the bug.
|
||||
- Expected behavior vs. actual behavior.
|
||||
- Your environment (runtime, database, adapter, bknd version).
|
||||
- Any relevant error messages or logs.
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||
- **GitHub Issues**: https://github.com/bknd-io/bknd/issues
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone who has contributed to bknd.
|
||||
|
||||
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
## Maintainers
|
||||
|
||||
- **Dennis** ([@dswbx](https://github.com/dswbx)) -- Creator and maintainer of bknd
|
||||
- **Cameron Pak** ([@cameronapak](https://github.com/cameronapak)) -- Maintainer of bknd docs
|
||||
+169
-77
@@ -1,110 +1,202 @@
|
||||
# Functional Source License, Version 1.1, MIT Future License
|
||||
|
||||
## Abbreviation
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
FSL-1.1-MIT
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
## Notice
|
||||
1. Definitions.
|
||||
|
||||
Copyright 2025 Dennis Senn
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
## Terms and Conditions
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
### Licensor ("We")
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
The party offering the Software under these Terms and Conditions.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
### The Software
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
The "Software" is each version of the software that we make available under
|
||||
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
||||
Conditions with the Software.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
### License Grant
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
Subject to your compliance with this License Grant and the Patents,
|
||||
Redistribution and Trademark clauses below, we hereby grant you the right to
|
||||
use, copy, modify, create derivative works, publicly perform, publicly display
|
||||
and redistribute the Software for any Permitted Purpose identified below.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
### Permitted Purpose
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
||||
means making the Software available to others in a commercial product or
|
||||
service that:
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
1. substitutes for the Software;
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
2. substitutes for any other product or service we offer using the Software
|
||||
that exists as of the date we make the Software available; or
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
3. offers the same or substantially similar functionality as the Software.
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
Permitted Purposes specifically include using the Software:
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
1. for your internal use and access;
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
2. for non-commercial education;
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
3. for non-commercial research; and
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
4. in connection with professional services that you provide to a licensee
|
||||
using the Software in accordance with these Terms and Conditions.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
### Patents
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
To the extent your use for a Permitted Purpose would necessarily infringe our
|
||||
patents, the license grant above includes a license under our patents. If you
|
||||
make a claim against any party that the Software infringes or contributes to
|
||||
the infringement of any patent, then your patent license to the Software ends
|
||||
immediately.
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Redistribution
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
The Terms and Conditions apply to all copies, modifications and derivatives of
|
||||
the Software.
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
If you redistribute any copies, modifications or derivatives of the Software,
|
||||
you must include a copy of or a link to these Terms and Conditions and not
|
||||
remove any copyright notices provided in or with the Software.
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
### Disclaimer
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
||||
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
||||
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
### Trademarks
|
||||
Copyright 2026 Dennis Senn
|
||||
|
||||
Except for displaying the License Details and identifying us as the origin of
|
||||
the Software, you have no right under these Terms and Conditions to use our
|
||||
trademarks, trade names, service marks or product names.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
## Grant of Future License
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
We hereby irrevocably grant you an additional license to use the Software under
|
||||
the MIT license that is effective on the second anniversary of the date we make
|
||||
the Software available. On or after that date, you may use the Software under
|
||||
the MIT license, in which case the following will apply:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -165,3 +165,19 @@ npx bknd run
|
||||
```bash
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. It covers dev setup, project structure, testing, code style, and how to submit changes.
|
||||
|
||||
Before writing code, please open a GitHub Issue or start a conversation in [Discord](https://discord.com/invite/952SFk8Tb8) so we can align on the approach.
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone who has contributed to bknd.
|
||||
|
||||
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
playwright-report
|
||||
test-results
|
||||
bknd.config.*
|
||||
__test__/helper.d.ts
|
||||
__test__/helper.d.ts
|
||||
.env.local
|
||||
@@ -1,66 +1,76 @@
|
||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||
import * as adapter from "adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
const stripConnection = <T extends Record<string, any>>(cfg: T) =>
|
||||
omitKeys(cfg, ["connection"]);
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", async () => {
|
||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(
|
||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||
).toEqual({});
|
||||
describe("makeConfig", () => {
|
||||
it("returns empty config for empty inputs", async () => {
|
||||
const cases: Array<Parameters<typeof adapter.makeConfig>> = [
|
||||
[{}],
|
||||
[{}, { env: { TEST: "test" } }],
|
||||
];
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(
|
||||
omitKeys(
|
||||
await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
for (const args of cases) {
|
||||
const cfg = await adapter.makeConfig(...(args as any));
|
||||
expect(stripConnection(cfg)).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it("allows all properties in app function", async () => {
|
||||
const called = mock(() => null);
|
||||
const config = await adapter.makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: {
|
||||
mode: "db",
|
||||
},
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ foo: "bar" },
|
||||
it("merges app output into config", async () => {
|
||||
const cfg = await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
);
|
||||
expect(config.connection).toEqual({ url: "test" });
|
||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(config.options).toEqual({ mode: "db" });
|
||||
await config.onBuilt?.(null as any);
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
expect(stripConnection(cfg)).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows all properties in app() result", async () => {
|
||||
const called = mock(() => null);
|
||||
|
||||
const cfg = await adapter.makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: { mode: "db" as const },
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ foo: "bar" },
|
||||
);
|
||||
|
||||
expect(cfg.connection).toEqual({ url: "test" });
|
||||
expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(cfg.options).toEqual({ mode: "db" });
|
||||
|
||||
await cfg.onBuilt?.({} as any);
|
||||
expect(called).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adapter test suites", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createFrameworkApp,
|
||||
label: "framework app",
|
||||
});
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createRuntimeApp,
|
||||
label: "runtime app",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,4 +70,10 @@ describe("Api", async () => {
|
||||
expect(params.token_transport).toBe("header");
|
||||
expect(params.host).toBe("http://another.com");
|
||||
});
|
||||
|
||||
it("should extract tokens case insensitive", async () => {
|
||||
const token = await sign({ sub: "test" }, "1234");
|
||||
expect(new Api({ headers: new Headers({ Authorization: `Bearer ${token}` }) }).getAuthState().token).toBe(token);
|
||||
expect(new Api({ headers: new Headers({ Authorization: `bearer ${token}` }) }).getAuthState().token).toBe(token);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference types="@types/bun" />
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it, mock } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
||||
import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
|
||||
import { MediaApi } from "media/api/MediaApi";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("MediaApi", () => {
|
||||
expect(isReadableStream(res.body)).toBe(true);
|
||||
expect(isReadableStream(res.res.body)).toBe(true);
|
||||
|
||||
const blob = await res.res.blob();
|
||||
const blob = (await res.res.blob()) as File;
|
||||
expect(isFile(blob)).toBe(true);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
expect(blob.type).toBe("image/png");
|
||||
@@ -110,10 +110,11 @@ describe("MediaApi", () => {
|
||||
const api = new MediaApi({}, mockedBackend.request);
|
||||
|
||||
const name = "image.png";
|
||||
const res = await api.getFileStream(name);
|
||||
expect(isReadableStream(res)).toBe(true);
|
||||
const res = await api.getFile(name);
|
||||
const stream = await api.getFileStream(name);
|
||||
expect(isReadableStream(stream)).toBe(true);
|
||||
|
||||
const blob = await new Response(res).blob();
|
||||
const blob = (await res.res.blob()) as File;
|
||||
expect(isFile(blob)).toBe(true);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
expect(blob.type).toBe("image/png");
|
||||
@@ -162,4 +163,30 @@ describe("MediaApi", () => {
|
||||
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
|
||||
}
|
||||
});
|
||||
|
||||
it("should add overwrite query for entity upload", async (c) => {
|
||||
const call = mock(() => null);
|
||||
const hono = new Hono().post(
|
||||
"/api/media/entity/:entity/:id/:field",
|
||||
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
||||
async (c) => {
|
||||
const { overwrite } = c.req.valid("query");
|
||||
expect(overwrite).toBe(true);
|
||||
call();
|
||||
return c.json({ ok: true });
|
||||
},
|
||||
);
|
||||
const api = new MediaApi(
|
||||
{
|
||||
upload_fetcher: hono.request,
|
||||
},
|
||||
hono.request,
|
||||
);
|
||||
const file = Bun.file(`${assetsPath}/image.png`);
|
||||
const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
|
||||
overwrite: true,
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect(call).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,4 +38,27 @@ describe("Authenticator", async () => {
|
||||
expect(cookie).toStartWith("auth=");
|
||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
||||
});
|
||||
|
||||
test("bearer token is case insensitive", async () => {
|
||||
const auth = new Authenticator({}, null as any, {
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
fields: ["sub"],
|
||||
},
|
||||
cookie: {
|
||||
sameSite: "strict",
|
||||
},
|
||||
});
|
||||
const token = await auth.jwt({ sub: "test" });
|
||||
|
||||
const res = await auth.resolveAuthFromRequest(new Headers({
|
||||
Authorization: `Bearer ${token}`,
|
||||
}));
|
||||
expect((res as any).sub).toBe("test")
|
||||
|
||||
const res2 = await auth.resolveAuthFromRequest(new Headers({
|
||||
Authorization: `bearer ${token}`,
|
||||
}));
|
||||
expect((res2 as any).sub).toBe("test")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
describe("PasswordStrategy", () => {
|
||||
it("should enforce provided minimum length", async () => {
|
||||
const strategy = new PasswordStrategy({ minLength: 8, hashing: "plain" });
|
||||
|
||||
expect(strategy.verify("password")({} as any)).rejects.toThrow();
|
||||
expect(
|
||||
strategy.verify("password1234")({ strategy_value: "password1234" } as any),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -41,12 +41,12 @@ describe("postgres", () => {
|
||||
beforeAll(async () => {
|
||||
if (!(await isPostgresRunning())) {
|
||||
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
|
||||
await $waitUntil("Postgres is running", isPostgresRunning);
|
||||
await $waitUntil("Postgres is running", isPostgresRunning, 500, 20);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
disableConsoleLog();
|
||||
});
|
||||
}, 30000);
|
||||
afterAll(async () => {
|
||||
if (await isPostgresRunning()) {
|
||||
try {
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("[data] WithBuilder", async () => {
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "author"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||
);
|
||||
expect(res2.parameters).toEqual([1]);
|
||||
});
|
||||
@@ -192,9 +192,7 @@ describe("[data] WithBuilder", async () => {
|
||||
{ single: {} },
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
||||
);
|
||||
expect(res.sql).toMatchSnapshot();
|
||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
@@ -204,9 +202,7 @@ describe("[data] WithBuilder", async () => {
|
||||
{ multiple: {} },
|
||||
);
|
||||
const res2 = qb2.compile();
|
||||
expect(res2.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
|
||||
);
|
||||
expect(res2.sql).toMatchSnapshot();
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||
});
|
||||
|
||||
@@ -271,9 +267,7 @@ describe("[data] WithBuilder", async () => {
|
||||
);
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
||||
);
|
||||
expect(qb.compile().sql).toMatchSnapshot();
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||
});
|
||||
|
||||
@@ -313,9 +307,7 @@ describe("[data] WithBuilder", async () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
);
|
||||
expect(qb.compile().sql).toMatchSnapshot();
|
||||
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`[data] WithBuilder recursive compiles with singles 1`] = `"select (select json_object('id', "obj"."id", 'username', "obj"."username", 'avatar', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object('id', "obj"."id", 'path', "obj"."path") from (select "avatar"."id" as "id", "avatar"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "avatar"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts""`;
|
||||
|
||||
exports[`[data] WithBuilder recursive compiles with many 1`] = `"select (select coalesce(json_group_array(json_object('id', "agg"."id", 'posts_id', "agg"."posts_id", 'users_id', "agg"."users_id", 'users', "agg"."users")), '[]') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object('username', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts""`;
|
||||
|
||||
exports[`[data] WithBuilder polymorphic 1`] = `"select (select json_object('id', "obj"."id", 'path', "obj"."path") from (select "single"."id" as "id", "single"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "single"."id" asc limit ?) as obj) as "single" from "categories""`;
|
||||
|
||||
exports[`[data] WithBuilder polymorphic 2`] = `"select (select coalesce(json_group_array(json_object('id', "agg"."id", 'path', "agg"."path")), '[]') from (select "multiple"."id" as "id", "multiple"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "multiple"."id" asc limit ? offset ?) as agg) as "multiple" from "categories""`;
|
||||
@@ -1,9 +1,15 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp, type AuthResponse } from "../../src";
|
||||
import { auth } from "../../src/modules/middlewares";
|
||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||
import {
|
||||
mergeObject,
|
||||
randomString,
|
||||
secureRandomString,
|
||||
withDisabledConsole,
|
||||
} from "../../src/core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import type { AppAuthSchema } from "auth/auth-schema";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -62,12 +68,12 @@ const configs = {
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
function createAuthApp(config?: Partial<AppAuthSchema>) {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
config: {
|
||||
auth: configs.auth,
|
||||
auth: mergeObject(configs.auth, config ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,6 +138,16 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
|
||||
return { res, data };
|
||||
},
|
||||
register: async (user: any): Promise<{ res: Response; data: AuthResponse }> => {
|
||||
const res = (await app.server.request("/api/auth/password/register", {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: body(user),
|
||||
})) as Response;
|
||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||
|
||||
return { res, data };
|
||||
},
|
||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||
const res = (await app.server.request("/api/auth/me", {
|
||||
method: "GET",
|
||||
@@ -245,4 +261,61 @@ describe("integration auth", () => {
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
}
|
||||
});
|
||||
|
||||
it("should register users with default role", async () => {
|
||||
const app = createAuthApp({ default_role_register: "guest" });
|
||||
await app.build();
|
||||
const $fns = fns(app);
|
||||
|
||||
// takes default role
|
||||
expect(
|
||||
await app
|
||||
.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
})
|
||||
.then((r) => r.role),
|
||||
).toBe("guest");
|
||||
|
||||
// throws error if role doesn't exist
|
||||
expect(
|
||||
app.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
role: "doesnt exist",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
// takes role if provided
|
||||
expect(
|
||||
await app
|
||||
.createUser({
|
||||
email: "test2@bknd.io",
|
||||
password: "12345678",
|
||||
role: "admin",
|
||||
})
|
||||
.then((r) => r.role),
|
||||
).toBe("admin");
|
||||
|
||||
// registering with role is not allowed
|
||||
expect(
|
||||
await $fns
|
||||
.register({
|
||||
email: "test3@bknd.io",
|
||||
password: "12345678",
|
||||
role: "admin",
|
||||
})
|
||||
.then((r) => r.res.ok),
|
||||
).toBe(false);
|
||||
|
||||
// takes default role
|
||||
expect(
|
||||
await $fns
|
||||
.register({
|
||||
email: "test3@bknd.io",
|
||||
password: "12345678",
|
||||
})
|
||||
.then((r) => r.data.user.role),
|
||||
).toBe("guest");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import * as proto from "data/prototype";
|
||||
|
||||
beforeAll(() => {
|
||||
disableConsoleLog();
|
||||
@@ -128,4 +129,87 @@ describe("MediaController", () => {
|
||||
expect(destFile.exists()).resolves.toBe(true);
|
||||
await destFile.delete();
|
||||
});
|
||||
|
||||
test("entity upload with max_items and overwrite", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
media: mergeObject(
|
||||
{
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: assetsTmpPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
),
|
||||
data: {
|
||||
entities: {
|
||||
posts: proto
|
||||
.entity("posts", {
|
||||
title: proto.text(),
|
||||
cover: proto.medium(),
|
||||
})
|
||||
.toJSON(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
// create a post first
|
||||
const createRes = await app.server.request("/api/data/entity/posts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: "Test Post" }),
|
||||
});
|
||||
expect(createRes.status).toBe(201);
|
||||
const { data: post } = (await createRes.json()) as any;
|
||||
|
||||
const file = Bun.file(path);
|
||||
const uploadedFiles: string[] = [];
|
||||
|
||||
// upload first file to entity (should succeed)
|
||||
const res1 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
expect(res1.status).toBe(201);
|
||||
const result1 = (await res1.json()) as any;
|
||||
uploadedFiles.push(result1.name);
|
||||
|
||||
// upload second file without overwrite (should fail - max_items reached)
|
||||
const res2 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
expect(res2.status).toBe(400);
|
||||
const result2 = (await res2.json()) as any;
|
||||
expect(result2.error).toContain("Max items");
|
||||
|
||||
// upload third file with overwrite=true (should succeed and delete old file)
|
||||
const res3 = await app.server.request(
|
||||
`/api/media/entity/posts/${post.id}/cover?overwrite=true`,
|
||||
{
|
||||
method: "POST",
|
||||
body: file,
|
||||
},
|
||||
);
|
||||
expect(res3.status).toBe(201);
|
||||
const result3 = (await res3.json()) as any;
|
||||
uploadedFiles.push(result3.name);
|
||||
|
||||
// verify old file was deleted from storage
|
||||
const oldFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[0]);
|
||||
expect(await oldFile.exists()).toBe(false);
|
||||
|
||||
// verify new file exists
|
||||
const newFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[1]);
|
||||
expect(await newFile.exists()).toBe(true);
|
||||
|
||||
// cleanup
|
||||
await newFile.delete();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("AppAuth", () => {
|
||||
});
|
||||
|
||||
await app.build();
|
||||
app.registerAdminController();
|
||||
app.registerAdminController({ forceDev: true });
|
||||
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
||||
|
||||
// register custom route
|
||||
@@ -223,4 +223,32 @@ describe("AppAuth", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("default role for registration must be a valid role", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "123456",
|
||||
},
|
||||
allow_register: true,
|
||||
roles: {
|
||||
guest: {
|
||||
is_default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
|
||||
const auth = app.module.auth;
|
||||
// doesn't allow invalid role
|
||||
expect(auth.schema().patch("default_role_register", "admin")).rejects.toThrow();
|
||||
// allows valid role
|
||||
await auth.schema().patch("default_role_register", "guest");
|
||||
expect(auth.toJSON().default_role_register).toBe("guest");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { AppReduced, type AppType } from "ui/client/utils/AppReduced";
|
||||
import type { BkndAdminProps } from "ui/Admin";
|
||||
|
||||
// Import the normalizeAdminPath function for testing
|
||||
// Note: This assumes the function is exported or we need to test it indirectly through public methods
|
||||
|
||||
describe("AppReduced", () => {
|
||||
let mockAppJson: AppType;
|
||||
let appReduced: AppReduced;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAppJson = {
|
||||
data: {
|
||||
entities: {},
|
||||
relations: {},
|
||||
},
|
||||
flows: {
|
||||
flows: {},
|
||||
},
|
||||
auth: {},
|
||||
} as AppType;
|
||||
});
|
||||
|
||||
describe("getSettingsPath", () => {
|
||||
it("should return settings path with basepath", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath();
|
||||
|
||||
expect(result).toBe("~/admin/settings");
|
||||
});
|
||||
|
||||
it("should return settings path with empty basepath", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath();
|
||||
|
||||
expect(result).toBe("~/settings");
|
||||
});
|
||||
|
||||
it("should append additional path segments", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath(["user", "profile"]);
|
||||
|
||||
expect(result).toBe("~/admin/settings/user/profile");
|
||||
});
|
||||
|
||||
it("should normalize multiple slashes", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "//admin//",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath(["//user//"]);
|
||||
|
||||
expect(result).toBe("~/admin/settings/user");
|
||||
});
|
||||
|
||||
it("should handle basepath without leading slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath();
|
||||
|
||||
expect(result).toBe("~/admin/settings");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAbsolutePath", () => {
|
||||
it("should return absolute path with basepath", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath("dashboard");
|
||||
|
||||
expect(result).toBe("~/admin/dashboard");
|
||||
});
|
||||
|
||||
it("should return base path when no path provided", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath();
|
||||
|
||||
expect(result).toBe("~/admin");
|
||||
});
|
||||
|
||||
it("should normalize paths correctly", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "//admin//",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath("//dashboard//");
|
||||
|
||||
expect(result).toBe("~/admin/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("options getter", () => {
|
||||
it("should return merged options with defaults", () => {
|
||||
const customOptions: BkndAdminProps["config"] = {
|
||||
basepath: "/custom-admin",
|
||||
logo_return_path: "/custom-home",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, customOptions);
|
||||
const options = appReduced.options;
|
||||
|
||||
expect(options).toEqual({
|
||||
logo_return_path: "/custom-home",
|
||||
basepath: "/custom-admin",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use default logo_return_path when not provided", () => {
|
||||
const customOptions: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, customOptions);
|
||||
const options = appReduced.options;
|
||||
|
||||
expect(options.logo_return_path).toBe("/");
|
||||
expect(options.basepath).toBe("/admin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("path normalization behavior", () => {
|
||||
it("should normalize duplicate slashes in settings path", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath(["//nested//path//"]);
|
||||
|
||||
expect(result).toBe("~/admin/settings/nested/path");
|
||||
});
|
||||
|
||||
it("should handle root path normalization", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath();
|
||||
|
||||
// The normalizeAdminPath function removes trailing slashes except for root "/"
|
||||
// When basepath is "/", the result is "~/" which becomes "~" after normalization
|
||||
expect(result).toBe("~");
|
||||
});
|
||||
|
||||
it("should preserve entity paths ending with slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath("entity/");
|
||||
|
||||
expect(result).toBe("~/admin/entity");
|
||||
});
|
||||
|
||||
it("should remove trailing slashes from non-entity paths", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath("dashboard/");
|
||||
|
||||
expect(result).toBe("~/admin/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("withBasePath - double slash fix (admin_basepath with trailing slash)", () => {
|
||||
it("should not produce double slashes when admin_basepath has trailing slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.withBasePath("/data");
|
||||
|
||||
expect(result).toBe("/admin/data");
|
||||
expect(result).not.toContain("//");
|
||||
});
|
||||
|
||||
it("should work correctly when admin_basepath has no trailing slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.withBasePath("/data");
|
||||
|
||||
expect(result).toBe("/admin/data");
|
||||
});
|
||||
|
||||
it("should handle absolute paths with admin_basepath trailing slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getAbsolutePath("data");
|
||||
|
||||
expect(result).toBe("~/admin/data");
|
||||
expect(result).not.toContain("//");
|
||||
});
|
||||
|
||||
it("should handle settings path with admin_basepath trailing slash", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath(["general"]);
|
||||
|
||||
expect(result).toBe("~/admin/settings/general");
|
||||
expect(result).not.toContain("//");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle undefined basepath", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath();
|
||||
|
||||
// When basepath is undefined, it defaults to empty string
|
||||
expect(result).toBe("~/settings");
|
||||
});
|
||||
|
||||
it("should handle null path segments", () => {
|
||||
const options: BkndAdminProps["config"] = {
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/",
|
||||
};
|
||||
|
||||
appReduced = new AppReduced(mockAppJson, options);
|
||||
const result = appReduced.getSettingsPath(["", "valid", ""]);
|
||||
|
||||
expect(result).toBe("~/admin/settings/valid");
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-12
@@ -3,17 +3,7 @@ import c from "picocolors";
|
||||
import { formatNumber } from "bknd/utils";
|
||||
|
||||
const deps = Object.keys(pkg.dependencies);
|
||||
const external = [
|
||||
"jsonv-ts/*",
|
||||
"wrangler",
|
||||
"bknd",
|
||||
"bknd/*",
|
||||
"@vitejs/plugin-react",
|
||||
"vite",
|
||||
"@tailwindcss/vite",
|
||||
"@cloudflare/vite-plugin",
|
||||
...deps,
|
||||
];
|
||||
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/cli/index.ts"],
|
||||
@@ -21,7 +11,6 @@ const result = await Bun.build({
|
||||
outdir: "./dist/cli",
|
||||
env: "PUBLIC_*",
|
||||
minify: true,
|
||||
banner: `const __originalLog=console.log;console.log=(...o)=>{const n=o[0];"string"==typeof n&&n.includes("[dotenv@")||__originalLog.apply(console,o)};`,
|
||||
external,
|
||||
define: {
|
||||
__isDev: "0",
|
||||
|
||||
+348
-255
@@ -2,6 +2,8 @@ import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
import { watch as fsWatch, readdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
@@ -12,163 +14,178 @@ const clean = args.includes("--clean");
|
||||
|
||||
// silence tsup
|
||||
const oldConsole = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
};
|
||||
console.log = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
const define = {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
};
|
||||
|
||||
if (clean) {
|
||||
console.info("Cleaning dist (w/o static)");
|
||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||
// Cross-platform clean: remove all files/folders in dist except static
|
||||
const distPath = join(import.meta.dir, "dist");
|
||||
try {
|
||||
const entries = readdirSync(distPath);
|
||||
for (const entry of entries) {
|
||||
if (entry === "static") continue;
|
||||
const entryPath = join(distPath, entry);
|
||||
rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
} catch (e) {
|
||||
// dist may not exist yet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
let types_running = false;
|
||||
function buildTypes() {
|
||||
if (types_running || !types) return;
|
||||
types_running = true;
|
||||
if (types_running || !types) return;
|
||||
types_running = true;
|
||||
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (types && !watch) {
|
||||
buildTypes();
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
let watcher_timeout: any;
|
||||
function delayTypes() {
|
||||
if (!watch || !types) return;
|
||||
if (watcher_timeout) {
|
||||
clearTimeout(watcher_timeout);
|
||||
}
|
||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||
if (!watch || !types) return;
|
||||
if (watcher_timeout) {
|
||||
clearTimeout(watcher_timeout);
|
||||
}
|
||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||
}
|
||||
|
||||
const dependencies = Object.keys(pkg.dependencies);
|
||||
|
||||
// collection of always-external packages
|
||||
const external = [
|
||||
...dependencies,
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"jsonv-ts",
|
||||
/^jsonv-ts\/.*/,
|
||||
...dependencies,
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"jsonv-ts",
|
||||
/^jsonv-ts\/.*/,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Building backend and general API
|
||||
*/
|
||||
async function buildApi() {
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
define,
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/core/utils/index.ts",
|
||||
"src/plugins/index.ts",
|
||||
"src/modes/index.ts",
|
||||
],
|
||||
outDir: "dist",
|
||||
external: [...external],
|
||||
metafile: true,
|
||||
target: "esnext",
|
||||
platform: "browser",
|
||||
removeNodeProtocol: false,
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
// don't use tsup's broken watch, we'll handle it ourselves
|
||||
watch: false,
|
||||
define,
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/core/utils/index.ts",
|
||||
"src/plugins/index.ts",
|
||||
"src/modes/index.ts",
|
||||
],
|
||||
outDir: "dist",
|
||||
external: [...external],
|
||||
metafile: true,
|
||||
target: "esnext",
|
||||
platform: "browser",
|
||||
removeNodeProtocol: false,
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function rewriteClient(path: string) {
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(
|
||||
path,
|
||||
'"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI for direct imports
|
||||
*/
|
||||
async function buildUi() {
|
||||
const base = {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
define,
|
||||
external: [
|
||||
...external,
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
} satisfies tsup.Options;
|
||||
const base = {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch: false,
|
||||
define,
|
||||
external: [
|
||||
...external,
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
} satisfies tsup.Options;
|
||||
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
outDir: "dist/ui/client",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
||||
},
|
||||
});
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
outDir: "dist/ui/client",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,162 +194,238 @@ async function buildUi() {
|
||||
* - ui/client is external, and after built replaced with "bknd/client"
|
||||
*/
|
||||
async function buildUiElements() {
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
define,
|
||||
entry: ["src/ui/elements/index.ts"],
|
||||
outDir: "dist/ui/elements",
|
||||
external: [
|
||||
"ui/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"wouter",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
||||
},
|
||||
});
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch: false,
|
||||
define,
|
||||
entry: ["src/ui/elements/index.ts"],
|
||||
outDir: "dist/ui/elements",
|
||||
external: [
|
||||
"ui/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"wouter",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Building adapters
|
||||
*/
|
||||
function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
|
||||
return {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: [`src/adapter/${adapter}/index.ts`],
|
||||
format: ["esm"],
|
||||
platform: "neutral",
|
||||
outDir: `dist/adapter/${adapter}`,
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
removeNodeProtocol: false,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
||||
},
|
||||
...overrides,
|
||||
define: {
|
||||
...define,
|
||||
...overrides.define,
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?hono.*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...external,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
function baseConfig(
|
||||
adapter: string,
|
||||
overrides: Partial<tsup.Options> = {},
|
||||
): tsup.Options {
|
||||
return {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch: false,
|
||||
entry: [`src/adapter/${adapter}/index.ts`],
|
||||
format: ["esm"],
|
||||
platform: "neutral",
|
||||
outDir: `dist/adapter/${adapter}`,
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
removeNodeProtocol: false,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
||||
},
|
||||
...overrides,
|
||||
define: {
|
||||
...define,
|
||||
...overrides.define,
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?hono.*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...external,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAdapters() {
|
||||
await Promise.all([
|
||||
// base adapter handles
|
||||
tsup.build({
|
||||
...baseConfig(""),
|
||||
target: "esnext",
|
||||
platform: "neutral",
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
// only way to keep @vite-ignore comments
|
||||
minify: false,
|
||||
}),
|
||||
await Promise.all([
|
||||
// base adapter handles
|
||||
tsup.build({
|
||||
...baseConfig(""),
|
||||
target: "esnext",
|
||||
platform: "neutral",
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
// only way to keep @vite-ignore comments
|
||||
minify: false,
|
||||
}),
|
||||
|
||||
// specific adatpers
|
||||
tsup.build(baseConfig("react-router")),
|
||||
tsup.build(
|
||||
baseConfig("browser", {
|
||||
external: [/^sqlocal\/?.*?/, "wouter"],
|
||||
}),
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
),
|
||||
tsup.build(baseConfig("astro")),
|
||||
tsup.build(baseConfig("aws")),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: ["wrangler", "node:process"],
|
||||
}),
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare/proxy", {
|
||||
target: "esnext",
|
||||
entry: ["src/adapter/cloudflare/proxy.ts"],
|
||||
outDir: "dist/adapter/cloudflare",
|
||||
metafile: false,
|
||||
external: [/bknd/, "wrangler", "node:process"],
|
||||
}),
|
||||
),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
// specific adatpers
|
||||
tsup.build(baseConfig("react-router")),
|
||||
tsup.build(
|
||||
baseConfig("browser", {
|
||||
external: [/^sqlocal\/?.*?/, "wouter"],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node",
|
||||
),
|
||||
tsup.build(baseConfig("astro")),
|
||||
tsup.build(baseConfig("aws")),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: ["wrangler", "node:process"],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/edge"),
|
||||
entry: ["src/adapter/sqlite/edge.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare/proxy", {
|
||||
target: "esnext",
|
||||
entry: ["src/adapter/cloudflare/proxy.ts"],
|
||||
outDir: "dist/adapter/cloudflare",
|
||||
metafile: false,
|
||||
external: [/bknd/, "wrangler", "node:process"],
|
||||
}),
|
||||
),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/node"),
|
||||
entry: ["src/adapter/sqlite/node.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
platform: "node",
|
||||
metafile: false,
|
||||
}),
|
||||
tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/bun"),
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
]);
|
||||
tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("tanstack-start"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sveltekit"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("nuxt"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/edge"),
|
||||
entry: ["src/adapter/sqlite/edge.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/node"),
|
||||
entry: ["src/adapter/sqlite/node.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
platform: "node",
|
||||
metafile: false,
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/bun"),
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
||||
async function buildAll() {
|
||||
await Promise.all([
|
||||
buildApi(),
|
||||
buildUi(),
|
||||
buildUiElements(),
|
||||
buildAdapters(),
|
||||
]);
|
||||
}
|
||||
|
||||
// initial build
|
||||
await buildAll();
|
||||
|
||||
// custom watcher since tsup's watch is broken in 8.3.5+
|
||||
if (watch) {
|
||||
oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let isBuilding = false;
|
||||
|
||||
const rebuild = async () => {
|
||||
if (isBuilding) return;
|
||||
isBuilding = true;
|
||||
oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
|
||||
try {
|
||||
await buildAll();
|
||||
oldConsole.log(c.cyan("[Watch]"), c.green("done"));
|
||||
} catch (e) {
|
||||
oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
|
||||
}
|
||||
isBuilding = false;
|
||||
};
|
||||
|
||||
const debouncedRebuild = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(rebuild, 100);
|
||||
};
|
||||
|
||||
// watch src directory recursively
|
||||
fsWatch(
|
||||
join(import.meta.dir, "src"),
|
||||
{ recursive: true },
|
||||
(event, filename) => {
|
||||
if (!filename) return;
|
||||
// ignore non-source files
|
||||
if (
|
||||
!filename.endsWith(".ts") &&
|
||||
!filename.endsWith(".tsx") &&
|
||||
!filename.endsWith(".css")
|
||||
)
|
||||
return;
|
||||
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
|
||||
debouncedRebuild();
|
||||
},
|
||||
);
|
||||
|
||||
// keep process alive
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
+3
-2
@@ -3,10 +3,11 @@ import path from "node:path";
|
||||
import c from "picocolors";
|
||||
|
||||
const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);
|
||||
type RunOptions = Omit<Bun.SpawnOptions.SpawnOptions<"ignore", "pipe", "pipe">, "stdout" | "stderr">;
|
||||
|
||||
async function run(
|
||||
cmd: string[] | string,
|
||||
opts: Bun.SpawnOptions.OptionsObject & {},
|
||||
opts: RunOptions,
|
||||
onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void,
|
||||
): Promise<{ proc: Bun.Subprocess; data: any }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -17,7 +18,7 @@ async function run(
|
||||
});
|
||||
|
||||
// Read from stdout
|
||||
const reader = (proc.stdout as ReadableStream).getReader();
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// Function to read chunks
|
||||
|
||||
+39
-33
@@ -1,44 +1,50 @@
|
||||
const adapter = process.env.TEST_ADAPTER;
|
||||
|
||||
const default_config = {
|
||||
media_adapter: "local",
|
||||
base_path: "",
|
||||
media_adapter: "local",
|
||||
base_path: "",
|
||||
} as const;
|
||||
|
||||
const configs = {
|
||||
cloudflare: {
|
||||
media_adapter: "r2",
|
||||
},
|
||||
"react-router": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
nextjs: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
astro: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
node: {
|
||||
base_path: "",
|
||||
},
|
||||
bun: {
|
||||
base_path: "",
|
||||
},
|
||||
cloudflare: {
|
||||
media_adapter: "r2",
|
||||
},
|
||||
"react-router": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
nextjs: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
nuxt: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
astro: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
node: {
|
||||
base_path: "",
|
||||
},
|
||||
bun: {
|
||||
base_path: "",
|
||||
},
|
||||
"tanstack-start": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
};
|
||||
|
||||
export function getAdapterConfig(): typeof default_config {
|
||||
if (adapter) {
|
||||
if (!configs[adapter]) {
|
||||
console.warn(
|
||||
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
...default_config,
|
||||
...configs[adapter],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (adapter) {
|
||||
if (!configs[adapter]) {
|
||||
console.warn(
|
||||
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
...default_config,
|
||||
...configs[adapter],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return default_config;
|
||||
return default_config;
|
||||
}
|
||||
|
||||
+25
-5
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.20.0-rc.1",
|
||||
"version": "0.21.0-rc.1",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
@@ -46,7 +46,7 @@
|
||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
||||
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
@@ -60,7 +60,7 @@
|
||||
"@xyflow/react": "^12.9.2",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs": "^1.11.20",
|
||||
"fast-xml-parser": "^5.3.1",
|
||||
"hono": "4.10.4",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
@@ -80,7 +80,6 @@
|
||||
"@aws-sdk/client-s3": "^3.922.0",
|
||||
"@bluwy/giget-core": "^0.1.6",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@cloudflare/vite-plugin": "^1.15.3",
|
||||
"@cloudflare/vitest-pool-workers": "^0.10.4",
|
||||
"@cloudflare/workers-types": "^4.20251014.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
@@ -143,7 +142,7 @@
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "3.0.9",
|
||||
"wouter": "^3.7.1",
|
||||
"wrangler": "^4.52.1"
|
||||
"wrangler": "^4.45.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.19.6"
|
||||
@@ -234,6 +233,11 @@
|
||||
"import": "./dist/adapter/nextjs/index.js",
|
||||
"require": "./dist/adapter/nextjs/index.js"
|
||||
},
|
||||
"./adapter/nuxt": {
|
||||
"types": "./dist/types/adapter/nuxt/index.d.ts",
|
||||
"import": "./dist/adapter/nuxt/index.js",
|
||||
"require": "./dist/adapter/nuxt/index.js"
|
||||
},
|
||||
"./adapter/react-router": {
|
||||
"types": "./dist/types/adapter/react-router/index.d.ts",
|
||||
"import": "./dist/adapter/react-router/index.js",
|
||||
@@ -254,6 +258,11 @@
|
||||
"import": "./dist/adapter/astro/index.js",
|
||||
"require": "./dist/adapter/astro/index.js"
|
||||
},
|
||||
"./adapter/sveltekit": {
|
||||
"types": "./dist/types/adapter/sveltekit/index.d.ts",
|
||||
"import": "./dist/adapter/sveltekit/index.js",
|
||||
"require": "./dist/adapter/sveltekit/index.js"
|
||||
},
|
||||
"./adapter/aws": {
|
||||
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||
"import": "./dist/adapter/aws/index.js",
|
||||
@@ -264,6 +273,11 @@
|
||||
"import": "./dist/adapter/browser/index.js",
|
||||
"require": "./dist/adapter/browser/index.js"
|
||||
},
|
||||
"./adapter/tanstack-start": {
|
||||
"types": "./dist/types/adapter/tanstack-start/index.d.ts",
|
||||
"import": "./dist/adapter/tanstack-start/index.js",
|
||||
"require": "./dist/adapter/tanstack-start/index.js"
|
||||
},
|
||||
"./dist/main.css": "./dist/ui/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||
@@ -278,9 +292,12 @@
|
||||
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
||||
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
|
||||
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
|
||||
"adapter/nuxt": ["./dist/types/adapter/nuxt/index.d.ts"],
|
||||
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
||||
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
||||
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
||||
"adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"],
|
||||
"adapter/tanstack-start": ["./dist/types/adapter/tanstack-start/index.d.ts"],
|
||||
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
||||
}
|
||||
},
|
||||
@@ -307,9 +324,12 @@
|
||||
"serverless",
|
||||
"cloudflare",
|
||||
"nextjs",
|
||||
"nuxt",
|
||||
"remix",
|
||||
"react-router",
|
||||
"astro",
|
||||
"sveltekit",
|
||||
"svelte",
|
||||
"bun",
|
||||
"node"
|
||||
]
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export class Api {
|
||||
}
|
||||
|
||||
// try authorization header
|
||||
const headerToken = this.options.headers.get("authorization")?.replace("Bearer ", "");
|
||||
const headerToken = this.options.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
||||
if (headerToken) {
|
||||
this.token_transport = "header";
|
||||
this.updateToken(headerToken);
|
||||
|
||||
@@ -69,11 +69,6 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
|
||||
if (Connection.isConnection(config.connection)) {
|
||||
connection = config.connection;
|
||||
} else {
|
||||
if (connection) {
|
||||
$console.warn(
|
||||
"Connection is not a valid connection object, using default SQLite connection",
|
||||
);
|
||||
}
|
||||
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
|
||||
const conf = appConfig.connection ?? { url: "file:data.db" };
|
||||
connection = sqlite(conf) as any;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./nuxt.adapter";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as nuxt from "./nuxt.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("nuxt adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: nuxt.getApp,
|
||||
makeHandler: nuxt.serve,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
|
||||
|
||||
export type NuxtEnv = NodeJS.ProcessEnv;
|
||||
export type NuxtBkndConfig<Env = NuxtEnv> = RuntimeBkndConfig<Env>;
|
||||
|
||||
/**
|
||||
* Get bknd app instance
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export async function getApp<Env>(
|
||||
config: NuxtBkndConfig<Env> = {} as NuxtBkndConfig<Env>,
|
||||
args: Env,
|
||||
) {
|
||||
return await createRuntimeApp(config, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create middleware handler for Nuxt
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export function serve<Env>(
|
||||
config: NuxtBkndConfig<Env> = {} as NuxtBkndConfig<Env>,
|
||||
args: Env,
|
||||
) {
|
||||
return async (request: Request) => {
|
||||
return (await getApp(config, args)).fetch(request);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sveltekit.adapter";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as sveltekit from "./sveltekit.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("sveltekit adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: (c, a) => sveltekit.getApp(c as any, a ?? ({} as any)),
|
||||
makeHandler: (c, a) => (request: Request) =>
|
||||
sveltekit.serve(c as any, a ?? ({} as any))({ request }),
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
|
||||
|
||||
type TSvelteKit = {
|
||||
request: Request;
|
||||
};
|
||||
|
||||
export type SvelteKitBkndConfig<Env> = Pick<RuntimeBkndConfig<Env>, "adminOptions">;
|
||||
|
||||
/**
|
||||
* Get bknd app instance
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables (use $env/dynamic/private for universal runtime support)
|
||||
*/
|
||||
export async function getApp<Env>(
|
||||
config: SvelteKitBkndConfig<Env> = {} as SvelteKitBkndConfig<Env>,
|
||||
args: Env,
|
||||
) {
|
||||
return await createRuntimeApp(config, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create request handler for hooks.server.ts
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables (use $env/dynamic/private for universal runtime support)
|
||||
*/
|
||||
export function serve<Env>(
|
||||
config: SvelteKitBkndConfig<Env> = {} as SvelteKitBkndConfig<Env>,
|
||||
args: Env,
|
||||
) {
|
||||
return async (fnArgs: TSvelteKit) => {
|
||||
return (await getApp(config, args)).fetch(fnArgs.request);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tanstack-start.adapter";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as tanstackStart from "./tanstack-start.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import type { TanstackStartConfig } from "./tanstack-start.adapter";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("tanstack start adapter", () => {
|
||||
adapterTestSuite<TanstackStartConfig>(bunTestRunner, {
|
||||
makeApp: tanstackStart.getApp,
|
||||
makeHandler: tanstackStart.serve,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
|
||||
|
||||
export type TanstackStartEnv = NodeJS.ProcessEnv;
|
||||
|
||||
export type TanstackStartConfig<Env = TanstackStartEnv> =
|
||||
FrameworkBkndConfig<Env>;
|
||||
|
||||
/**
|
||||
* Get bknd app instance
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export async function getApp<Env = TanstackStartEnv>(
|
||||
config: TanstackStartConfig<Env> = {},
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return await createFrameworkApp(config, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create request handler for src/routes/api.$.ts
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export function serve<Env = TanstackStartEnv>(
|
||||
config: TanstackStartConfig<Env> = {},
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return async (request: Request) => {
|
||||
const app = await getApp(config, args);
|
||||
return app.fetch(request);
|
||||
};
|
||||
}
|
||||
+29
-1
@@ -46,6 +46,22 @@ export class AppAuth extends Module<AppAuthSchema> {
|
||||
to.strategies!.password!.enabled = true;
|
||||
}
|
||||
|
||||
if (to.default_role_register && to.default_role_register?.length > 0) {
|
||||
const valid_to_role = Object.keys(to.roles ?? {}).includes(to.default_role_register);
|
||||
|
||||
if (!valid_to_role) {
|
||||
const msg = `Default role for registration not found: ${to.default_role_register}`;
|
||||
// if changing to a new value
|
||||
if (from.default_role_register !== to.default_role_register) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// resetting gracefully, since role doesn't exist anymore
|
||||
$console.warn(`${msg}, resetting to undefined`);
|
||||
to.default_role_register = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return to;
|
||||
}
|
||||
|
||||
@@ -82,6 +98,7 @@ export class AppAuth extends Module<AppAuthSchema> {
|
||||
this._authenticator = new Authenticator(strategies, new AppUserPool(this), {
|
||||
jwt: this.config.jwt,
|
||||
cookie: this.config.cookie,
|
||||
default_role_register: this.config.default_role_register,
|
||||
});
|
||||
|
||||
this.registerEntities();
|
||||
@@ -171,10 +188,20 @@ export class AppAuth extends Module<AppAuthSchema> {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async createUser({ email, password, ...additional }: CreateUserPayload): Promise<DB["users"]> {
|
||||
async createUser({
|
||||
email,
|
||||
password,
|
||||
role,
|
||||
...additional
|
||||
}: CreateUserPayload): Promise<DB["users"]> {
|
||||
if (!this.enabled) {
|
||||
throw new Error("Cannot create user, auth not enabled");
|
||||
}
|
||||
if (role) {
|
||||
if (!Object.keys(this.config.roles ?? {}).includes(role)) {
|
||||
throw new Error(`Role "${role}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const strategy = "password" as const;
|
||||
const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
|
||||
@@ -183,6 +210,7 @@ export class AppAuth extends Module<AppAuthSchema> {
|
||||
mutator.__unstable_toggleSystemEntityCreation(false);
|
||||
const { data: created } = await mutator.insertOne({
|
||||
...(additional as any),
|
||||
role: role || this.config.default_role_register || undefined,
|
||||
email,
|
||||
strategy,
|
||||
strategy_value,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
InvalidSchemaError,
|
||||
transformObject,
|
||||
mcpTool,
|
||||
$console,
|
||||
} from "bknd/utils";
|
||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||
|
||||
@@ -198,7 +199,12 @@ export class AuthController extends Controller {
|
||||
for (const [name, strategy] of Object.entries(strategies)) {
|
||||
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
||||
|
||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||
hono.route(
|
||||
`/${name}`,
|
||||
strategy.getController(this.auth.authenticator, {
|
||||
allow_register: this.auth.config.allow_register,
|
||||
}),
|
||||
);
|
||||
this.registerStrategyActions(strategy, hono);
|
||||
}
|
||||
|
||||
@@ -210,7 +216,7 @@ export class AuthController extends Controller {
|
||||
const idType = s.anyOf([s.number({ title: "Integer" }), s.string({ title: "UUID" })]);
|
||||
|
||||
const getUser = async (params: { id?: string | number; email?: string }) => {
|
||||
let user: DB["users"] | undefined = undefined;
|
||||
let user: DB["users"] | undefined;
|
||||
if (params.id) {
|
||||
const { data } = await this.userRepo.findId(params.id);
|
||||
user = data;
|
||||
@@ -225,26 +231,33 @@ export class AuthController extends Controller {
|
||||
};
|
||||
|
||||
const roles = Object.keys(this.auth.config.roles ?? {});
|
||||
mcp.tool(
|
||||
"auth_user_create",
|
||||
{
|
||||
description: "Create a new user",
|
||||
inputSchema: s.object({
|
||||
email: s.string({ format: "email" }),
|
||||
password: s.string({ minLength: 8 }),
|
||||
role: s
|
||||
.string({
|
||||
enum: roles.length > 0 ? roles : undefined,
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.createUser);
|
||||
try {
|
||||
const actions = this.auth.authenticator.strategy("password").getActions();
|
||||
if (actions.create) {
|
||||
const schema = actions.create.schema;
|
||||
mcp.tool(
|
||||
"auth_user_create",
|
||||
{
|
||||
description: "Create a new user",
|
||||
inputSchema: s.object({
|
||||
...schema.properties,
|
||||
role: s
|
||||
.string({
|
||||
enum: roles.length > 0 ? roles : undefined,
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.createUser);
|
||||
|
||||
return c.json(await this.auth.createUser(params));
|
||||
},
|
||||
);
|
||||
return c.json(await this.auth.createUser(params));
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
$console.warn("error creating auth_user_create tool", e);
|
||||
}
|
||||
|
||||
mcp.tool(
|
||||
"auth_user_token",
|
||||
@@ -297,7 +310,9 @@ export class AuthController extends Controller {
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.testPassword);
|
||||
|
||||
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||
const controller = pw.getController(this.auth.authenticator);
|
||||
const controller = pw.getController(this.auth.authenticator, {
|
||||
allow_register: this.auth.config.allow_register,
|
||||
});
|
||||
|
||||
const res = await controller.request(
|
||||
new Request("https://localhost/login", {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const authConfigSchema = $object(
|
||||
basepath: s.string({ default: "/api/auth" }),
|
||||
entity_name: s.string({ default: "users" }),
|
||||
allow_register: s.boolean({ default: true }).optional(),
|
||||
default_role_register: s.string().optional(),
|
||||
jwt: jwtConfig,
|
||||
cookie: cookieConfig,
|
||||
strategies: $record(
|
||||
|
||||
@@ -74,6 +74,7 @@ export const jwtConfig = s.strictObject(
|
||||
export const authenticatorConfig = s.object({
|
||||
jwt: jwtConfig,
|
||||
cookie: cookieConfig,
|
||||
default_role_register: s.string().optional(),
|
||||
});
|
||||
|
||||
type AuthConfig = s.Static<typeof authenticatorConfig>;
|
||||
@@ -164,9 +165,13 @@ export class Authenticator<
|
||||
if (!("strategy_value" in profile)) {
|
||||
throw new InvalidConditionsException("Profile must have a strategy value");
|
||||
}
|
||||
if ("role" in profile) {
|
||||
throw new InvalidConditionsException("Role cannot be provided during registration");
|
||||
}
|
||||
|
||||
const user = await this.userPool.create(strategy.getName(), {
|
||||
...profile,
|
||||
role: this.config.default_role_register,
|
||||
strategy_value: profile.strategy_value,
|
||||
});
|
||||
|
||||
@@ -425,7 +430,7 @@ export class Authenticator<
|
||||
let token: string | undefined;
|
||||
if (headers.has("Authorization")) {
|
||||
const bearerHeader = String(headers.get("Authorization"));
|
||||
token = bearerHeader.replace("Bearer ", "");
|
||||
token = bearerHeader.replace(/^Bearer\s+/i, "");
|
||||
} else {
|
||||
const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context);
|
||||
token = await this.getAuthCookie(context);
|
||||
|
||||
@@ -10,6 +10,7 @@ const schema = s
|
||||
.object({
|
||||
hashing: s.string({ enum: ["plain", "sha256", "bcrypt"], default: "sha256" }),
|
||||
rounds: s.number({ minimum: 1, maximum: 10 }).optional(),
|
||||
minLength: s.number({ default: 8, minimum: 1 }).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -37,7 +38,7 @@ export class PasswordStrategy extends AuthStrategy<typeof schema> {
|
||||
format: "email",
|
||||
}),
|
||||
password: s.string({
|
||||
minLength: 8, // @todo: this should be configurable
|
||||
minLength: this.config.minLength,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -65,19 +66,28 @@ export class PasswordStrategy extends AuthStrategy<typeof schema> {
|
||||
return await bcryptCompare(compare, actual);
|
||||
}
|
||||
|
||||
return false;
|
||||
return actual === compare;
|
||||
}
|
||||
|
||||
verify(password: string) {
|
||||
return async (user: User) => {
|
||||
const compare = await this.compare(user?.strategy_value!, password);
|
||||
if (!user || !user.strategy_value) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if (!this.getPayloadSchema().properties.password.validate(password).valid) {
|
||||
$console.debug("PasswordStrategy: Invalid password", password);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const compare = await this.compare(user.strategy_value, password);
|
||||
if (compare !== true) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getController(authenticator: Authenticator): Hono<any> {
|
||||
getController(authenticator: Authenticator, opts: { allow_register?: boolean }): Hono<any> {
|
||||
const hono = new Hono();
|
||||
const redirectQuerySchema = s.object({
|
||||
redirect: s.string().optional(),
|
||||
@@ -110,41 +120,43 @@ export class PasswordStrategy extends AuthStrategy<typeof schema> {
|
||||
},
|
||||
);
|
||||
|
||||
hono.post(
|
||||
"/register",
|
||||
describeRoute({
|
||||
summary: "Register a new user with email and password",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
jsc("query", redirectQuerySchema),
|
||||
async (c) => {
|
||||
try {
|
||||
const { redirect } = c.req.valid("query");
|
||||
const { password, email, ...body } = parse(
|
||||
payloadSchema,
|
||||
await authenticator.getBody(c),
|
||||
{
|
||||
onError: (errors) => {
|
||||
$console.error("Invalid register payload", [...errors]);
|
||||
new InvalidCredentialsException();
|
||||
if (opts.allow_register) {
|
||||
hono.post(
|
||||
"/register",
|
||||
describeRoute({
|
||||
summary: "Register a new user with email and password",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
jsc("query", redirectQuerySchema),
|
||||
async (c) => {
|
||||
try {
|
||||
const { redirect } = c.req.valid("query");
|
||||
const { password, email, ...body } = parse(
|
||||
payloadSchema,
|
||||
await authenticator.getBody(c),
|
||||
{
|
||||
onError: (errors) => {
|
||||
$console.error("Invalid register payload", [...errors]);
|
||||
new InvalidCredentialsException();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const profile = {
|
||||
...body,
|
||||
email,
|
||||
strategy_value: await this.hash(password),
|
||||
};
|
||||
const profile = {
|
||||
...body,
|
||||
email,
|
||||
strategy_value: await this.hash(password),
|
||||
};
|
||||
|
||||
return await authenticator.resolveRegister(c, this, profile, async () => void 0, {
|
||||
redirect,
|
||||
});
|
||||
} catch (e) {
|
||||
return authenticator.respondWithError(c, e as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
return await authenticator.resolveRegister(c, this, profile, async () => void 0, {
|
||||
redirect,
|
||||
});
|
||||
} catch (e) {
|
||||
return authenticator.respondWithError(c, e as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return hono;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export abstract class AuthStrategy<Schema extends s.Schema = s.Schema> {
|
||||
|
||||
protected abstract getSchema(): Schema;
|
||||
|
||||
abstract getController(auth: Authenticator): Hono;
|
||||
abstract getController(auth: Authenticator, opts: { allow_register?: boolean }): Hono;
|
||||
|
||||
getType(): string {
|
||||
return this.type;
|
||||
|
||||
@@ -284,7 +284,7 @@ export class OAuthStrategy extends AuthStrategy<typeof schemaProvided> {
|
||||
}
|
||||
}
|
||||
|
||||
getController(auth: Authenticator): Hono<any> {
|
||||
getController(auth: Authenticator, opts: { allow_register?: boolean }): Hono<any> {
|
||||
const hono = new Hono();
|
||||
const secret = "secret";
|
||||
const cookie_name = "_challenge";
|
||||
@@ -379,6 +379,10 @@ export class OAuthStrategy extends AuthStrategy<typeof schemaProvided> {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
if (action === "register" && !opts.allow_register) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const url = new URL(c.req.url);
|
||||
const path = url.pathname.replace(`/${action}`, "");
|
||||
const redirect_uri = url.origin + path + "/callback";
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { CliCommand } from "cli/types";
|
||||
import { Option } from "commander";
|
||||
import { withConfigOptions, type WithConfigOptions } from "cli/utils/options";
|
||||
import * as utils from "./utils";
|
||||
import { $console } from "core/utils";
|
||||
import { Project } from "./lib/Project";
|
||||
|
||||
export const dev: CliCommand = (program) =>
|
||||
withConfigOptions(program.command("dev"))
|
||||
.description("dev server")
|
||||
.addOption(new Option("--build", "build the project"))
|
||||
.action(action);
|
||||
|
||||
async function action(options: WithConfigOptions<{ build?: boolean }>) {
|
||||
console.log("options", options);
|
||||
const project = new Project({
|
||||
templatePath: utils.TEMPLATE_PATH,
|
||||
});
|
||||
|
||||
await project.init();
|
||||
|
||||
if (options.build) {
|
||||
await project.build();
|
||||
process.exit(0);
|
||||
}
|
||||
await project.listen();
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { RelativeFS } from "./RelativeFS";
|
||||
//import { $console } from "bknd/utils";
|
||||
import { type ViteDevServer, createServer, type UserConfig, createBuilder } from "vite";
|
||||
import { readdir, copyFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { cloudflare } from "@cloudflare/vite-plugin";
|
||||
import { getRelativeDistPath } from "cli/utils/sys";
|
||||
import { injectMain } from "./vite/inject-main";
|
||||
import { devFsVitePlugin } from "adapter/cloudflare";
|
||||
|
||||
export type ProjectOptions = {
|
||||
userPath?: string;
|
||||
templatePath?: string;
|
||||
};
|
||||
|
||||
// @todo: add multiple public dirs
|
||||
// @todo: add npm install (first time)
|
||||
// @todo: add package.json
|
||||
export class Project {
|
||||
public userFs: RelativeFS;
|
||||
public templateFs: RelativeFS;
|
||||
private _server?: ViteDevServer;
|
||||
|
||||
constructor(public options: ProjectOptions) {
|
||||
this.userFs = new RelativeFS(options.userPath ?? process.cwd());
|
||||
this.templateFs = new RelativeFS(options.templatePath ?? "src/cli/commands/dev/template");
|
||||
}
|
||||
|
||||
get server() {
|
||||
if (!this._server) {
|
||||
throw new Error("Server not initialized");
|
||||
}
|
||||
return this._server!;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const source = this.templateFs.root;
|
||||
const destination = this.userFs.root;
|
||||
|
||||
// recursively copy all files and directories from source to dest
|
||||
const copyRecursive = async (src: string, dst: string) => {
|
||||
const entries = await readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const dstPath = path.join(dst, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.userFs.makeDir(path.relative(destination, dstPath));
|
||||
await copyRecursive(srcPath, dstPath);
|
||||
} else if (entry.isFile()) {
|
||||
const exists = await stat(dstPath)
|
||||
.then((s) => s.isFile())
|
||||
.catch(() => null);
|
||||
|
||||
// only copy everything from ".bknd" with override
|
||||
if (!exists || (dstPath.includes(".bknd") && !dstPath.includes("bknd-types.d.ts"))) {
|
||||
await copyFile(srcPath, dstPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await copyRecursive(source, destination);
|
||||
this._server = await createServer(this.getViteConfig());
|
||||
}
|
||||
|
||||
private getViteConfig(): UserConfig {
|
||||
return {
|
||||
clearScreen: false,
|
||||
publicDir: getRelativeDistPath() + "/static",
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
devFsVitePlugin({ configFile: ".bknd/bknd.config.ts" }) as any,
|
||||
cloudflare({
|
||||
configPath: this.userFs.path(".bknd/wrangler.json"),
|
||||
persistState: {
|
||||
path: this.userFs.path(".bknd/state"),
|
||||
},
|
||||
}),
|
||||
injectMain({
|
||||
appPath: "./src/App.tsx",
|
||||
rootId: "root",
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
minify: true,
|
||||
},
|
||||
resolve: {
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async build() {
|
||||
const builder = await createBuilder(this.getViteConfig());
|
||||
await builder.buildApp();
|
||||
}
|
||||
|
||||
async listen() {
|
||||
await this.server.listen();
|
||||
this.server.printUrls();
|
||||
this.server.bindCLIShortcuts({ print: true });
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
mkdir,
|
||||
stat,
|
||||
readFile as nodeReadFile,
|
||||
writeFile as nodeWriteFile,
|
||||
} from "node:fs/promises";
|
||||
import { getRootPath } from "cli/utils/sys";
|
||||
import path from "node:path";
|
||||
|
||||
export class RelativeFS {
|
||||
public root: string;
|
||||
|
||||
constructor(p: string) {
|
||||
this.root = path.resolve(getRootPath(), p);
|
||||
}
|
||||
|
||||
path(p: string) {
|
||||
return path.join(this.root, p);
|
||||
}
|
||||
|
||||
async hasFile(path: string) {
|
||||
try {
|
||||
const s = await stat(this.path(path));
|
||||
return s.isFile();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hasDir(path: string) {
|
||||
try {
|
||||
const s = await stat(this.path(path));
|
||||
return s.isDirectory();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(p: string) {
|
||||
return await nodeReadFile(this.path(p), "utf-8");
|
||||
}
|
||||
|
||||
async writeFile(p: string, content: string) {
|
||||
return await nodeWriteFile(this.path(p), content);
|
||||
}
|
||||
|
||||
async makeDir(p: string) {
|
||||
return await mkdir(this.path(p), { recursive: true });
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { PluginOption } from "vite";
|
||||
|
||||
export function injectMain(options?: { appPath?: string; rootId?: string }): PluginOption {
|
||||
const appPath = options?.appPath ?? "/App.tsx";
|
||||
const rootId = options?.rootId ?? "root";
|
||||
const publicId = "/@virtual/react-main.js";
|
||||
const internalId = "\0virtual-react-main.js";
|
||||
|
||||
return [
|
||||
{
|
||||
name: "bknd-virtual-react-entry",
|
||||
transformIndexHtml(html) {
|
||||
return {
|
||||
html,
|
||||
tags: [
|
||||
{
|
||||
tag: "script",
|
||||
injectTo: "body",
|
||||
attrs: {
|
||||
type: "module",
|
||||
src: publicId,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === publicId) {
|
||||
return internalId;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
load(id) {
|
||||
if (id === internalId) {
|
||||
return `
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "${appPath}";
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
const container = document.getElementById("${rootId}");
|
||||
if (!container) {
|
||||
throw new Error("Cannot find #${rootId} element");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
React.createElement(
|
||||
React.StrictMode,
|
||||
null,
|
||||
React.createElement(ClientProvider, null, React.createElement(App, null))
|
||||
)
|
||||
);
|
||||
`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { DB } from "bknd";
|
||||
import type { Insertable, Selectable, Updateable } from "kysely";
|
||||
|
||||
declare global {
|
||||
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
|
||||
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
|
||||
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
import config from "./bknd.config";
|
||||
|
||||
export default serve(config);
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "bknd-dev",
|
||||
"main": "./worker.ts",
|
||||
"compatibility_date": "2025-10-08",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": "../dist/client",
|
||||
"not_found_handling": "single-page-application",
|
||||
"run_worker_first": ["!/", "/admin*", "/api*", "!/assets/*"]
|
||||
},
|
||||
"vars": {
|
||||
"ENVIRONMENT": "development"
|
||||
},
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "BUCKET"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bknd + Vite + Cloudflare + React + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
import "./styles.css";
|
||||
|
||||
export default function App() {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Hono } from "hono";
|
||||
import type { ServerEnv } from "bknd";
|
||||
|
||||
/**
|
||||
* Add custom routes to the API here. Base path is `/api`.
|
||||
*/
|
||||
export default new Hono<ServerEnv>().get("/", (c) => {
|
||||
// const app = c.var.app;
|
||||
// const api = app.getApi();
|
||||
return c.json({ message: "Hello, world!" });
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { AppEvents, DatabaseEvents, type EventManager } from "bknd";
|
||||
|
||||
export default function (emgr: EventManager) {
|
||||
// emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||
// console.log("Request received", event.params.request.url);
|
||||
// });
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { em, entity, text, boolean, number, datetime, json, jsonSchema, enumm } from "bknd";
|
||||
|
||||
export default em({
|
||||
// todos: entity("todos", {
|
||||
// title: text(),
|
||||
// done: boolean(),
|
||||
// }),
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./.bknd/tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
mkdir,
|
||||
stat,
|
||||
readFile as nodeReadFile,
|
||||
writeFile as nodeWriteFile,
|
||||
} from "node:fs/promises";
|
||||
import { getRootPath } from "cli/utils/sys";
|
||||
|
||||
export const TEMPLATE_PATH = "src/cli/commands/dev/template";
|
||||
export const currentDir = process.cwd();
|
||||
|
||||
export const fs = (dir: string) => {
|
||||
const PATH = path.resolve(getRootPath(), dir);
|
||||
return {
|
||||
PATH,
|
||||
path: (_path: string) => path.join(PATH, _path),
|
||||
hasFile: async (file: string) => {
|
||||
try {
|
||||
const s = await stat(path.join(PATH, file));
|
||||
return s.isFile();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
hasDir: async (_dir: string) => {
|
||||
try {
|
||||
const s = await stat(path.join(PATH, _dir));
|
||||
return s.isDirectory();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
readFile: (file: string) => nodeReadFile(path.join(PATH, file), "utf-8"),
|
||||
readJsonFile: async (file: string) =>
|
||||
JSON.parse(await nodeReadFile(path.join(PATH, file), "utf-8")),
|
||||
writeFile: (file: string, content: string) => nodeWriteFile(path.join(PATH, file), content),
|
||||
makeDir: (_newDir: string) => mkdir(path.join(PATH, _newDir)),
|
||||
};
|
||||
};
|
||||
@@ -9,4 +9,3 @@ export { types } from "./types";
|
||||
export { mcp } from "./mcp/mcp";
|
||||
export { sync } from "./sync";
|
||||
export { secrets } from "./secrets";
|
||||
export { dev } from "./dev/dev";
|
||||
|
||||
@@ -21,46 +21,47 @@ export const sync: CliCommand = (program) => {
|
||||
console.info("");
|
||||
if (stmts.length === 0) {
|
||||
console.info(c.yellow("No changes to sync"));
|
||||
process.exit(0);
|
||||
}
|
||||
// @todo: currently assuming parameters aren't used
|
||||
const sql = stmts.map((d) => d.sql).join(";\n") + ";";
|
||||
} else {
|
||||
// @todo: currently assuming parameters aren't used
|
||||
const sql = stmts.map((d) => d.sql).join(";\n") + ";";
|
||||
|
||||
if (options.force) {
|
||||
console.info(c.dim("Executing:") + "\n" + c.cyan(sql));
|
||||
await schema.sync({ force: true, drop: options.drop });
|
||||
if (options.force) {
|
||||
console.info(c.dim("Executing:") + "\n" + c.cyan(sql));
|
||||
await schema.sync({ force: true, drop: options.drop });
|
||||
|
||||
console.info(`\n${c.dim(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
|
||||
console.info(`${c.green("Database synced")}`);
|
||||
|
||||
if (options.seed) {
|
||||
console.info(c.dim("\nExecuting seed..."));
|
||||
const seed = app.options?.seed;
|
||||
if (seed) {
|
||||
await app.options?.seed?.({
|
||||
...app.modules.ctx(),
|
||||
app: app,
|
||||
});
|
||||
console.info(c.green("Seed executed"));
|
||||
console.info(`\n${c.dim(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
|
||||
console.info(`${c.green("Database synced")}`);
|
||||
} else {
|
||||
if (options.out) {
|
||||
const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
|
||||
await writeFile(options.out, output);
|
||||
console.info(`SQL written to ${c.cyan(options.out)}`);
|
||||
} else {
|
||||
console.info(c.yellow("No seed function provided"));
|
||||
console.info(c.dim("DDL to execute:") + "\n" + c.cyan(sql));
|
||||
|
||||
console.info(
|
||||
c.yellow(
|
||||
"\nNo statements have been executed. Use --force to perform database syncing operations",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.out) {
|
||||
const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
|
||||
await writeFile(options.out, output);
|
||||
console.info(`SQL written to ${c.cyan(options.out)}`);
|
||||
} else {
|
||||
console.info(c.dim("DDL to execute:") + "\n" + c.cyan(sql));
|
||||
}
|
||||
|
||||
console.info(
|
||||
c.yellow(
|
||||
"\nNo statements have been executed. Use --force to perform database syncing operations",
|
||||
),
|
||||
);
|
||||
if (options.seed) {
|
||||
console.info(c.dim("\nExecuting seed..."));
|
||||
const seed = app.options?.seed;
|
||||
if (seed) {
|
||||
await seed({
|
||||
...app.modules.ctx(),
|
||||
app: app,
|
||||
});
|
||||
console.info(c.green("Seed executed"));
|
||||
} else {
|
||||
console.info(c.yellow("No seed function provided"));
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from "commander";
|
||||
import color from "picocolors";
|
||||
import * as commands from "./commands";
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ export function isDebug(): boolean {
|
||||
try {
|
||||
// @ts-expect-error - this is a global variable in dev
|
||||
return is_toggled(__isDev);
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export class WithBuilder {
|
||||
if (query) {
|
||||
subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, {
|
||||
ignore: ["with", cardinality === 1 ? "limit" : undefined].filter(Boolean) as any,
|
||||
alias: other.reference,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,11 +71,12 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
||||
}
|
||||
|
||||
protected uploadFile<T extends FileUploadedEventData>(
|
||||
body: File | Blob | ReadableStream | Buffer<ArrayBufferLike>,
|
||||
body: BodyInit,
|
||||
opts?: {
|
||||
filename?: string;
|
||||
path?: TInput;
|
||||
_init?: Omit<RequestInit, "body">;
|
||||
query?: Record<string, any>;
|
||||
},
|
||||
): FetchPromise<ResponseObject<T>> {
|
||||
const headers = {
|
||||
@@ -102,14 +103,22 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
||||
headers,
|
||||
};
|
||||
if (opts?.path) {
|
||||
return this.post(opts.path, body, init);
|
||||
return this.request<T>(opts.path, opts?.query, {
|
||||
...init,
|
||||
body,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
if (!name || name.length === 0) {
|
||||
throw new Error("Invalid filename");
|
||||
}
|
||||
|
||||
return this.post<T>(opts?.path ?? ["upload", name], body, init);
|
||||
return this.request<T>(opts?.path ?? ["upload", name], opts?.query, {
|
||||
...init,
|
||||
body,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
async upload<T extends FileUploadedEventData>(
|
||||
@@ -119,6 +128,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
||||
_init?: Omit<RequestInit, "body">;
|
||||
path?: TInput;
|
||||
fetcher?: ApiFetcher;
|
||||
query?: Record<string, any>;
|
||||
} = {},
|
||||
) {
|
||||
if (item instanceof Request || typeof item === "string") {
|
||||
@@ -144,7 +154,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
||||
});
|
||||
}
|
||||
|
||||
return this.uploadFile<T>(item, opts);
|
||||
return this.uploadFile<T>(item as BodyInit, opts);
|
||||
}
|
||||
|
||||
async uploadToEntity(
|
||||
@@ -155,11 +165,14 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
||||
opts?: {
|
||||
_init?: Omit<RequestInit, "body">;
|
||||
fetcher?: typeof fetch;
|
||||
overwrite?: boolean;
|
||||
},
|
||||
): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> {
|
||||
const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
|
||||
return this.upload(item, {
|
||||
...opts,
|
||||
path: ["entity", entity, id, field],
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { css, Style } from "hono/css";
|
||||
import { Controller } from "modules/Controller";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import type { TApiUser } from "Api";
|
||||
import type { AppTheme } from "ui/client/use-theme";
|
||||
import type { Manifest } from "vite";
|
||||
|
||||
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
||||
@@ -20,7 +21,7 @@ export type AdminBkndWindowContext = {
|
||||
logout_route: string;
|
||||
admin_basepath: string;
|
||||
logo_return_path?: string;
|
||||
theme?: "dark" | "light" | "system";
|
||||
theme?: AppTheme;
|
||||
};
|
||||
|
||||
// @todo: add migration to remove admin path from config
|
||||
@@ -31,7 +32,7 @@ export type AdminControllerOptions = {
|
||||
html?: string;
|
||||
forceDev?: boolean | { mainPath: string };
|
||||
debugRerenders?: boolean;
|
||||
theme?: "dark" | "light" | "system";
|
||||
theme?: AppTheme;
|
||||
logoReturnPath?: string;
|
||||
manifest?: Manifest;
|
||||
};
|
||||
@@ -122,7 +123,7 @@ export class AdminController extends Controller {
|
||||
const obj: AdminBkndWindowContext = {
|
||||
user: c.get("auth")?.user,
|
||||
logout_route: authRoutes.logout,
|
||||
admin_basepath: this.options.adminBasepath,
|
||||
admin_basepath: this.options.adminBasepath.replace(/\/+$/, ""),
|
||||
theme: this.options.theme,
|
||||
logo_return_path: this.options.logoReturnPath,
|
||||
};
|
||||
@@ -209,8 +210,9 @@ export class AdminController extends Controller {
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
} else {
|
||||
const manifestPath = "bknd/dist/manifest.json";
|
||||
// @ts-ignore
|
||||
manifest = await import("bknd/dist/manifest.json", {
|
||||
manifest = await import(/* @vite-ignore */ manifestPath, {
|
||||
with: { type: "json" },
|
||||
}).then((res) => res.default);
|
||||
}
|
||||
@@ -304,7 +306,7 @@ const wrapperStyle = css`
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: rgb(9,9,11);
|
||||
background-color: rgb(250,250,250);
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color: rgb(250,250,250);
|
||||
background-color: rgb(30,31,34);
|
||||
|
||||
@@ -153,7 +153,7 @@ export function emailOTP({
|
||||
"/login",
|
||||
jsc(
|
||||
"json",
|
||||
s.object({
|
||||
s.strictObject({
|
||||
email: s.string({ format: "email" }),
|
||||
code: s.string({ minLength: 1 }).optional(),
|
||||
}),
|
||||
@@ -213,7 +213,7 @@ export function emailOTP({
|
||||
),
|
||||
jsc("query", s.object({ redirect: s.string().optional() })),
|
||||
async (c) => {
|
||||
const { email, code } = c.req.valid("json");
|
||||
const { email, code, ...rest } = c.req.valid("json");
|
||||
const { redirect } = c.req.valid("query");
|
||||
|
||||
// throw if user exists
|
||||
@@ -232,6 +232,7 @@ export function emailOTP({
|
||||
await em.mutator(entityName).updateOne(otpData.id, { used_at: new Date() });
|
||||
|
||||
const user = await app.createUser({
|
||||
...rest,
|
||||
email,
|
||||
password: randomString(32, true),
|
||||
});
|
||||
|
||||
@@ -71,4 +71,54 @@ describe("timestamps plugin", () => {
|
||||
expect(updatedData.updated_at).toBeDefined();
|
||||
expect(updatedData.updated_at).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test("index strategy", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
data: em({
|
||||
posts: entity("posts", {
|
||||
title: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
options: {
|
||||
plugins: [timestamps({ entities: ["posts"] })],
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
expect(app.em.indices.length).toBe(0);
|
||||
{
|
||||
const app = createApp({
|
||||
config: {
|
||||
data: em({
|
||||
posts: entity("posts", {
|
||||
title: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
options: {
|
||||
plugins: [timestamps({ entities: ["posts"], indexStrategy: "composite" })],
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
expect(app.em.indices.length).toBe(1);
|
||||
}
|
||||
|
||||
{
|
||||
const app = createApp({
|
||||
config: {
|
||||
data: em({
|
||||
posts: entity("posts", {
|
||||
title: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
options: {
|
||||
plugins: [timestamps({ entities: ["posts"], indexStrategy: "individual" })],
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
expect(app.em.indices.length).toBe(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { $console } from "bknd/utils";
|
||||
export type TimestampsPluginOptions = {
|
||||
entities: string[];
|
||||
setUpdatedOnCreate?: boolean;
|
||||
indexStrategy?: "composite" | "individual";
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,7 @@ export type TimestampsPluginOptions = {
|
||||
export function timestamps({
|
||||
entities = [],
|
||||
setUpdatedOnCreate = true,
|
||||
indexStrategy,
|
||||
}: TimestampsPluginOptions): AppPlugin {
|
||||
return (app: App) => ({
|
||||
name: "timestamps",
|
||||
@@ -29,19 +31,35 @@ export function timestamps({
|
||||
}
|
||||
|
||||
const appEntities = app.em.entities.map((e) => e.name);
|
||||
const actualEntities = entities.filter((e) => appEntities.includes(e));
|
||||
|
||||
return em(
|
||||
Object.fromEntries(
|
||||
entities
|
||||
.filter((e) => appEntities.includes(e))
|
||||
.map((e) => [
|
||||
e,
|
||||
entity(e, {
|
||||
created_at: datetime(),
|
||||
updated_at: datetime(),
|
||||
}),
|
||||
]),
|
||||
actualEntities.map((e) => [
|
||||
e,
|
||||
entity(e, {
|
||||
created_at: datetime(),
|
||||
updated_at: datetime(),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
(fns, schema) => {
|
||||
if (indexStrategy) {
|
||||
for (const entity of actualEntities) {
|
||||
if (entity in schema) {
|
||||
switch (indexStrategy) {
|
||||
case "composite":
|
||||
fns.index(schema[entity]!).on(["created_at", "updated_at"]);
|
||||
break;
|
||||
case "individual":
|
||||
fns.index(schema[entity]!).on(["created_at"]);
|
||||
fns.index(schema[entity]!).on(["updated_at"]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onBuilt: async () => {
|
||||
|
||||
@@ -22,7 +22,6 @@ export class AppReduced {
|
||||
protected appJson: AppType,
|
||||
protected _options: BkndAdminProps["config"] = {},
|
||||
) {
|
||||
//console.log("received appjson", appJson);
|
||||
|
||||
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
||||
return constructEntity(name, entity);
|
||||
@@ -70,20 +69,27 @@ export class AppReduced {
|
||||
|
||||
get options() {
|
||||
return {
|
||||
basepath: "",
|
||||
basepath: "/",
|
||||
logo_return_path: "/",
|
||||
...this._options,
|
||||
};
|
||||
}
|
||||
|
||||
withBasePath(path: string | string[], absolute = false): string {
|
||||
const paths = Array.isArray(path) ? path : [path];
|
||||
return [absolute ? "~" : null, this.options.basepath, ...paths]
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/")
|
||||
.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
getSettingsPath(path: string[] = []): string {
|
||||
const base = `~/${this.options.basepath}/settings`.replace(/\/+/g, "/");
|
||||
return [base, ...path].join("/");
|
||||
return this.withBasePath(["settings", ...path], true);
|
||||
}
|
||||
|
||||
getAbsolutePath(path?: string): string {
|
||||
const { basepath } = this.options;
|
||||
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
|
||||
return this.withBasePath(path ?? [], true);
|
||||
}
|
||||
|
||||
getAuthConfig() {
|
||||
|
||||
@@ -103,7 +103,7 @@ export function AuthForm({
|
||||
</Group>
|
||||
<Group>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Password name="password" required minLength={8} />
|
||||
<Password name="password" required minLength={1} />
|
||||
</Group>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -474,6 +474,7 @@ type SectionHeaderAccordionItemProps = {
|
||||
ActiveIcon?: any;
|
||||
children?: React.ReactNode;
|
||||
renderHeaderRight?: (props: { open: boolean }) => React.ReactNode;
|
||||
scrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const SectionHeaderAccordionItem = ({
|
||||
@@ -483,6 +484,7 @@ export const SectionHeaderAccordionItem = ({
|
||||
ActiveIcon = IconChevronUp,
|
||||
children,
|
||||
renderHeaderRight,
|
||||
scrollContainerRef,
|
||||
}: SectionHeaderAccordionItemProps) => (
|
||||
<div
|
||||
style={{ minHeight: 49 }}
|
||||
@@ -493,6 +495,8 @@ export const SectionHeaderAccordionItem = ({
|
||||
: "flex-initial cursor-pointer hover:bg-primary/5",
|
||||
)}
|
||||
>
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: . */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: . */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-row bg-muted/10 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2",
|
||||
@@ -501,14 +505,12 @@ export const SectionHeaderAccordionItem = ({
|
||||
>
|
||||
<IconButton Icon={open ? ActiveIcon : IconChevronDown} disabled={open} />
|
||||
<h2 className="text-lg dark:font-bold font-semibold select-text">{title}</h2>
|
||||
<div className="flex flex-grow" />
|
||||
<div className="flex grow" />
|
||||
{renderHeaderRight?.({ open })}
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"overflow-y-scroll transition-all",
|
||||
open ? " flex-grow" : "h-0 opacity-0",
|
||||
)}
|
||||
ref={scrollContainerRef}
|
||||
className={twMerge("overflow-y-scroll transition-all", open ? " grow" : "h-0 opacity-0")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -518,14 +520,25 @@ export const SectionHeaderAccordionItem = ({
|
||||
export const RouteAwareSectionHeaderAccordionItem = ({
|
||||
routePattern,
|
||||
identifier,
|
||||
renderHeaderRight,
|
||||
...props
|
||||
}: Omit<SectionHeaderAccordionItemProps, "open" | "toggle"> & {
|
||||
}: Omit<SectionHeaderAccordionItemProps, "open" | "toggle" | "renderHeaderRight"> & {
|
||||
renderHeaderRight?: (props: { open: boolean; active: boolean }) => React.ReactNode;
|
||||
// it's optional because it could be provided using the context
|
||||
routePattern?: string;
|
||||
identifier: string;
|
||||
}) => {
|
||||
const { active, toggle } = useRoutePathState(routePattern, identifier);
|
||||
return <SectionHeaderAccordionItem {...props} open={active} toggle={toggle} />;
|
||||
return (
|
||||
<SectionHeaderAccordionItem
|
||||
{...props}
|
||||
open={active}
|
||||
toggle={toggle}
|
||||
renderHeaderRight={
|
||||
renderHeaderRight && ((props) => renderHeaderRight?.({ open: props.open, active }))
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Separator = ({ className, ...props }: ComponentPropsWithoutRef<"hr">) => (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SegmentedControl, Tooltip } from "@mantine/core";
|
||||
import { SegmentedControl } from "@mantine/core";
|
||||
import { IconApi, IconBook, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
||||
import {
|
||||
TbDatabase,
|
||||
@@ -24,34 +24,35 @@ import { useLocation } from "wouter";
|
||||
import { NavLink } from "./AppShell";
|
||||
import { autoFormatString } from "core/utils";
|
||||
import { appShellStore } from "ui/store";
|
||||
import { getVersion } from "core/env";
|
||||
import { getVersion, isDebug } from "core/env";
|
||||
import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon";
|
||||
import { useAppShellAdminOptions } from "ui/options";
|
||||
|
||||
export function HeaderNavigation() {
|
||||
const [location, navigate] = useLocation();
|
||||
const { config } = useBknd();
|
||||
|
||||
const items: {
|
||||
label: string;
|
||||
href: string;
|
||||
Icon: any;
|
||||
Icon?: any;
|
||||
exact?: boolean;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
/*{
|
||||
label: "Base",
|
||||
href: "#",
|
||||
exact: true,
|
||||
Icon: TbLayoutDashboard,
|
||||
disabled: true,
|
||||
tooltip: "Coming soon"
|
||||
},*/
|
||||
{ label: "Data", href: "/data", Icon: TbDatabase },
|
||||
{ label: "Auth", href: "/auth", Icon: TbFingerprint },
|
||||
{ label: "Media", href: "/media", Icon: TbPhoto },
|
||||
{ label: "Flows", href: "/flows", Icon: TbHierarchy2 },
|
||||
];
|
||||
|
||||
if (isDebug() || Object.keys(config.flows?.flows ?? {}).length > 0) {
|
||||
items.push({ label: "Flows", href: "/flows", Icon: TbHierarchy2 });
|
||||
}
|
||||
|
||||
if (config.server.mcp.enabled) {
|
||||
items.push({ label: "MCP", href: "/tools/mcp", Icon: McpIcon });
|
||||
}
|
||||
|
||||
const activeItem = items.find((item) =>
|
||||
item.exact ? location === item.href : location.startsWith(item.href),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import Admin from "./Admin";
|
||||
//import "./main.css";
|
||||
import "./styles.css";
|
||||
|
||||
function render() {
|
||||
|
||||
@@ -378,11 +378,7 @@ function replaceEntitiesEnum(schema: Record<string, any>, entities: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
const Policy = ({
|
||||
permission,
|
||||
}: {
|
||||
permission: TPermission;
|
||||
}) => {
|
||||
const Policy = ({ permission }: { permission: TPermission }) => {
|
||||
const { value } = useDerivedFieldContext("", ({ value }) => ({
|
||||
effect: (value?.effect ?? "allow") as "allow" | "deny" | "filter",
|
||||
}));
|
||||
@@ -503,22 +499,24 @@ const CustomFieldWrapper = ({
|
||||
className: "max-w-none",
|
||||
}}
|
||||
position="bottom-end"
|
||||
target={() =>
|
||||
typeof schema.content === "object" ? (
|
||||
<JsonViewer
|
||||
className="w-auto max-w-120 bg-background pr-3 text-sm"
|
||||
json={schema.content}
|
||||
title={schema.name}
|
||||
expand={5}
|
||||
/>
|
||||
) : (
|
||||
<CodePreview
|
||||
code={schema.content}
|
||||
lang="typescript"
|
||||
className="w-auto max-w-120 bg-background p-3 text-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
target={() => (
|
||||
<div className="w-auto max-w-[80vw] md:max-w-120 bg-background overflow-scroll">
|
||||
{typeof schema.content === "object" ? (
|
||||
<JsonViewer
|
||||
className="pr-3 text-sm"
|
||||
json={schema.content}
|
||||
title={schema.name}
|
||||
expand={5}
|
||||
/>
|
||||
) : (
|
||||
<CodePreview
|
||||
code={schema.content}
|
||||
lang="typescript"
|
||||
className="p-3 text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button variant="ghost" size="smaller" IconLeft={TbCodeDots}>
|
||||
{autoFormatString(schema.name)}
|
||||
|
||||
@@ -15,7 +15,12 @@ import { useBkndWindowContext } from "bknd/client";
|
||||
import ToolsRoutes from "./tools";
|
||||
|
||||
// @ts-ignore
|
||||
const TestRoutes = lazy(() => import("./test"));
|
||||
let TestRoutes: any;
|
||||
try {
|
||||
if (import.meta.env.DEV) {
|
||||
TestRoutes = lazy(() => import("./test"));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
export function Routes({
|
||||
BkndWrapper,
|
||||
@@ -28,7 +33,7 @@ export function Routes({
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const ctx = useBkndWindowContext();
|
||||
const actualBasePath = basePath || ctx.admin_basepath;
|
||||
const actualBasePath = (basePath || ctx.admin_basepath).replace(/\/+$/, "");
|
||||
|
||||
return (
|
||||
<div id="bknd-admin" className={theme + " antialiased"}>
|
||||
@@ -43,11 +48,13 @@ export function Routes({
|
||||
<Route path="/" nest>
|
||||
<Root>
|
||||
<Switch>
|
||||
<Route path="/test*" nest>
|
||||
<Suspense fallback={null}>
|
||||
<TestRoutes />
|
||||
</Suspense>
|
||||
</Route>
|
||||
{TestRoutes && (
|
||||
<Route path="/test*" nest>
|
||||
<Suspense fallback={null}>
|
||||
<TestRoutes />
|
||||
</Suspense>
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
|
||||
@@ -49,19 +49,21 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
|
||||
try {
|
||||
const user_entity = config.entity_name ?? "users";
|
||||
const entities = _s.config.data.entities ?? {};
|
||||
console.log("entities", entities, user_entity);
|
||||
const user_fields = Object.entries(entities[user_entity]?.fields ?? {})
|
||||
.map(([name, field]) => (!field.config?.virtual ? name : undefined))
|
||||
.filter(Boolean);
|
||||
|
||||
if (user_fields.length > 0) {
|
||||
console.log("user_fields", user_fields);
|
||||
_schema.properties.jwt.properties.fields.items.enum = user_fields;
|
||||
_schema.properties.jwt.properties.fields.uniqueItems = true;
|
||||
uiSchema.jwt.fields["ui:widget"] = "checkboxes";
|
||||
}
|
||||
} catch (e) {}
|
||||
console.log("_s", _s);
|
||||
|
||||
const roles = Object.keys(config.roles ?? {});
|
||||
if (roles.length > 0) {
|
||||
_schema.properties.default_role_register.enum = roles;
|
||||
}
|
||||
} catch (_e) {}
|
||||
const roleSchema = _schema.properties.roles?.additionalProperties ?? { type: "object" };
|
||||
/* if (_s.permissions) {
|
||||
roleSchema.properties.permissions.items.enum = _s.permissions;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function ToolsRoutes() {
|
||||
return (
|
||||
<>
|
||||
<Route path="/" component={ToolsIndex} />
|
||||
<Route path="/mcp" component={ToolsMcp} />
|
||||
<Route path="/mcp*" component={ToolsMcp} nest />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import { Empty } from "ui/components/display/Empty";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { appShellStore } from "ui/store";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
||||
import { Route, Switch } from "wouter";
|
||||
|
||||
export default function ToolsMcp() {
|
||||
useBrowserTitle(["MCP UI"]);
|
||||
|
||||
const { config } = useBknd();
|
||||
const feature = useMcpStore((state) => state.feature);
|
||||
const setFeature = useMcpStore((state) => state.setFeature);
|
||||
const content = useMcpStore((state) => state.content);
|
||||
const openSidebar = appShellStore((store) => store.toggleSidebar("default"));
|
||||
const mcpPath = config.server.mcp.path;
|
||||
|
||||
@@ -29,51 +28,57 @@ export default function ToolsMcp() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow max-w-screen">
|
||||
<AppShell.SectionHeader>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<McpIcon />
|
||||
<AppShell.SectionHeaderTitle className="whitespace-nowrap truncate">
|
||||
MCP UI
|
||||
</AppShell.SectionHeaderTitle>
|
||||
<div className="hidden md:flex flex-row gap-2 items-center bg-primary/5 rounded-full px-3 pr-3.5 py-2">
|
||||
<TbWorld />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-mono leading-none select-text">
|
||||
{window.location.origin + mcpPath}
|
||||
</span>
|
||||
<RoutePathStateProvider path={"/:type?"} defaultIdentifier="tools">
|
||||
<div className="flex flex-col flex-grow max-w-screen">
|
||||
<AppShell.SectionHeader>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<McpIcon />
|
||||
<AppShell.SectionHeaderTitle className="whitespace-nowrap truncate">
|
||||
MCP UI
|
||||
</AppShell.SectionHeaderTitle>
|
||||
<div className="hidden md:flex flex-row gap-2 items-center bg-primary/5 rounded-full px-3 pr-3.5 py-2">
|
||||
<TbWorld />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-mono leading-none select-text">
|
||||
{window.location.origin + mcpPath}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell.SectionHeader>
|
||||
</AppShell.SectionHeader>
|
||||
|
||||
<div className="flex h-full">
|
||||
<AppShell.Sidebar>
|
||||
<Tools.Sidebar open={feature === "tools"} toggle={() => setFeature("tools")} />
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
title="Resources"
|
||||
open={feature === "resources"}
|
||||
toggle={() => setFeature("resources")}
|
||||
>
|
||||
<div className="flex flex-col flex-grow p-3 gap-3 justify-center items-center opacity-40">
|
||||
<i>Resources</i>
|
||||
</div>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
</AppShell.Sidebar>
|
||||
{feature === "tools" && <Tools.Content />}
|
||||
|
||||
{!content && (
|
||||
<Empty title="No tool selected" description="Please select a tool to continue.">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => openSidebar()}
|
||||
className="block md:hidden"
|
||||
<div className="flex grow h-full">
|
||||
<AppShell.Sidebar>
|
||||
<Tools.Sidebar />
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
title="Resources"
|
||||
identifier="resources"
|
||||
>
|
||||
Open Tools
|
||||
</Button>
|
||||
</Empty>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow p-3 gap-3 justify-center items-center opacity-40">
|
||||
<i>Resources</i>
|
||||
</div>
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
</AppShell.Sidebar>
|
||||
|
||||
<Switch>
|
||||
<Route path="/tools/:toolName?" component={Tools.Content} />
|
||||
<Route path="*">
|
||||
<Empty
|
||||
title="No tool selected"
|
||||
description="Please select a tool to continue."
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => openSidebar()}
|
||||
className="block md:hidden"
|
||||
>
|
||||
Open Tools
|
||||
</Button>
|
||||
</Empty>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RoutePathStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,23 +3,16 @@ import { combine } from "zustand/middleware";
|
||||
|
||||
import type { ToolJson } from "jsonv-ts/mcp";
|
||||
|
||||
const FEATURES = ["tools", "resources"] as const;
|
||||
export type Feature = (typeof FEATURES)[number];
|
||||
|
||||
export const useMcpStore = create(
|
||||
combine(
|
||||
{
|
||||
tools: [] as ToolJson[],
|
||||
feature: "tools" as Feature | null,
|
||||
content: null as ToolJson | null,
|
||||
history: [] as { type: "request" | "response"; data: any }[],
|
||||
historyLimit: 50,
|
||||
historyVisible: false,
|
||||
},
|
||||
(set) => ({
|
||||
setTools: (tools: ToolJson[]) => set({ tools }),
|
||||
setFeature: (feature: Feature) => set({ feature }),
|
||||
setContent: (content: ToolJson | null) => set({ content }),
|
||||
addHistory: (type: "request" | "response", data: any) =>
|
||||
set((state) => ({
|
||||
history: [{ type, data }, ...state.history.slice(0, state.historyLimit - 1)],
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AppShell } from "ui/layouts/AppShell";
|
||||
import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { JsonViewer, JsonViewerTabs, type JsonViewerTabsRef } from "ui/components/code/JsonViewer";
|
||||
import { twMerge } from "ui/elements/mocks/tailwind-merge";
|
||||
import { Field, Form } from "ui/components/form/json-schema-form";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
@@ -13,17 +12,18 @@ import { appShellStore } from "ui/store";
|
||||
import { Icon } from "ui/components/display/Icon";
|
||||
import { useMcpClient } from "./hooks/use-mcp-client";
|
||||
import { Tooltip } from "@mantine/core";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useParams } from "wouter";
|
||||
|
||||
export function Sidebar({ open, toggle }) {
|
||||
export function Sidebar() {
|
||||
const client = useMcpClient();
|
||||
const closeSidebar = appShellStore((store) => store.closeSidebar("default"));
|
||||
const tools = useMcpStore((state) => state.tools);
|
||||
const setTools = useMcpStore((state) => state.setTools);
|
||||
const setContent = useMcpStore((state) => state.setContent);
|
||||
const content = useMcpStore((state) => state.content);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(undefined!);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -39,15 +39,22 @@ export function Sidebar({ open, toggle }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleRefresh();
|
||||
handleRefresh().then(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const selectedTool = scrollContainerRef.current.querySelector(".active");
|
||||
if (selectedTool) {
|
||||
selectedTool.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
title="Tools"
|
||||
open={open}
|
||||
toggle={toggle}
|
||||
renderHeaderRight={() => (
|
||||
identifier="tools"
|
||||
renderHeaderRight={({ active }) => (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{error && (
|
||||
<Tooltip label={error}>
|
||||
@@ -57,7 +64,7 @@ export function Sidebar({ open, toggle }) {
|
||||
<span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none">
|
||||
{tools.length}
|
||||
</span>
|
||||
<IconButton Icon={TbRefresh} disabled={!open || loading} onClick={handleRefresh} />
|
||||
<IconButton Icon={TbRefresh} disabled={!active || loading} onClick={handleRefresh} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
@@ -76,12 +83,11 @@ export function Sidebar({ open, toggle }) {
|
||||
return (
|
||||
<AppShell.SidebarLink
|
||||
key={tool.name}
|
||||
className={twMerge(
|
||||
"flex flex-col items-start h-auto py-3 gap-px",
|
||||
content?.name === tool.name ? "active" : "",
|
||||
)}
|
||||
className="flex flex-col items-start h-auto py-3 gap-px"
|
||||
as={Link}
|
||||
href={`/tools/${tool.name}`}
|
||||
onClick={() => {
|
||||
setContent(tool);
|
||||
//setContent(tool);
|
||||
closeSidebar();
|
||||
}}
|
||||
>
|
||||
@@ -92,32 +98,34 @@ export function Sidebar({ open, toggle }) {
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Content() {
|
||||
const content = useMcpStore((state) => state.content);
|
||||
const { toolName } = useParams();
|
||||
const tools = useMcpStore((state) => state.tools);
|
||||
const tool = tools.find((tool) => tool.name === toolName);
|
||||
const addHistory = useMcpStore((state) => state.addHistory);
|
||||
const [payload, setPayload] = useState<object>(getTemplate(content?.inputSchema));
|
||||
const [payload, setPayload] = useState<object>(getTemplate(tool?.inputSchema));
|
||||
const [result, setResult] = useState<object | null>(null);
|
||||
const historyVisible = useMcpStore((state) => state.historyVisible);
|
||||
const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible);
|
||||
const client = useMcpClient();
|
||||
const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null);
|
||||
const hasInputSchema =
|
||||
content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0;
|
||||
tool?.inputSchema && Object.keys(tool.inputSchema.properties ?? {}).length > 0;
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
setPayload(getTemplate(content?.inputSchema));
|
||||
setPayload(getTemplate(tool?.inputSchema));
|
||||
setResult(null);
|
||||
}, [content]);
|
||||
}, [toolName]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!content?.name) return;
|
||||
if (!tool?.name) return;
|
||||
const request = {
|
||||
name: content.name,
|
||||
name: tool.name,
|
||||
arguments: payload,
|
||||
};
|
||||
startTransition(async () => {
|
||||
@@ -131,7 +139,7 @@ export function Content() {
|
||||
});
|
||||
}, [payload]);
|
||||
|
||||
if (!content) return null;
|
||||
if (!tool) return null;
|
||||
|
||||
let readableResult = result;
|
||||
try {
|
||||
@@ -144,11 +152,11 @@ export function Content() {
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="flex flex-grow flex-col min-w-0 max-w-screen"
|
||||
key={content.name}
|
||||
className="flex grow flex-col min-w-0 max-w-screen"
|
||||
key={tool.name}
|
||||
schema={{
|
||||
title: "InputSchema",
|
||||
...content?.inputSchema,
|
||||
...tool?.inputSchema,
|
||||
}}
|
||||
validateOn="submit"
|
||||
initialValues={payload}
|
||||
@@ -163,12 +171,12 @@ export function Content() {
|
||||
right={
|
||||
<div className="flex flex-row gap-2">
|
||||
<IconButton
|
||||
Icon={historyVisible ? TbHistory : TbHistoryOff}
|
||||
Icon={historyVisible ? TbHistoryOff : TbHistory}
|
||||
onClick={() => setHistoryVisible(!historyVisible)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!content?.name || isPending}
|
||||
disabled={!tool?.name || isPending}
|
||||
variant="primary"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
@@ -181,19 +189,19 @@ export function Content() {
|
||||
<span className="opacity-50">
|
||||
Tools <span className="opacity-70">/</span>
|
||||
</span>{" "}
|
||||
<span className="truncate">{content?.name}</span>
|
||||
<span className="truncate">{tool?.name}</span>
|
||||
</AppShell.SectionHeaderTitle>
|
||||
</AppShell.SectionHeader>
|
||||
<div className="flex flex-grow flex-row w-vw">
|
||||
<div className="flex grow flex-row w-vw">
|
||||
<div
|
||||
className="flex flex-grow flex-col max-w-full"
|
||||
className="flex grow flex-col max-w-full"
|
||||
style={{
|
||||
width: "calc(100% - var(--sidebar-width-right) - 1px)",
|
||||
}}
|
||||
>
|
||||
<AppShell.Scrollable>
|
||||
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
|
||||
<p className="text-primary/80">{content?.description}</p>
|
||||
<div key={JSON.stringify(tool)} className="flex flex-col py-4 px-5 gap-4">
|
||||
<p className="text-primary/80">{tool?.description}</p>
|
||||
|
||||
{hasInputSchema && <Field name="" />}
|
||||
<JsonViewerTabs
|
||||
@@ -209,7 +217,7 @@ export function Content() {
|
||||
},
|
||||
Result: { json: readableResult, title: "Result" },
|
||||
Configuration: {
|
||||
json: content ?? null,
|
||||
json: tool ?? null,
|
||||
title: "Configuration",
|
||||
},
|
||||
}}
|
||||
@@ -234,7 +242,7 @@ const History = () => {
|
||||
<>
|
||||
<AppShell.SectionHeader>History</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-col flex-grow p-3 gap-1">
|
||||
<div className="flex grow flex-col p-3 gap-1">
|
||||
{history.map((item, i) => (
|
||||
<JsonViewer
|
||||
key={`${item.type}-${i}`}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
declare module '*.css';
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": ".",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./node_modules", "./__test__", "./e2e"]
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"rootDir": "./src",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./node_modules", "./__test__", "./e2e"]
|
||||
}
|
||||
|
||||
+2
-3
@@ -2,11 +2,11 @@
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"module": "ESNext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"allowImportingTsExtensions": false,
|
||||
"target": "ES2022",
|
||||
"target": "es2023",
|
||||
"noImplicitAny": false,
|
||||
"allowJs": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
@@ -26,7 +26,6 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist/types",
|
||||
"paths": {
|
||||
"*": ["./src/*"],
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bknd",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.3.3",
|
||||
"@tsconfig/strictest": "^2.0.7",
|
||||
"@tsconfig/strictest": "^2.0.8",
|
||||
"@types/bun": "^1.3.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"miniflare": "^3.20250718.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "^6.0.2",
|
||||
"verdaccio": "^6.2.1",
|
||||
},
|
||||
},
|
||||
"app": {
|
||||
"name": "bknd",
|
||||
"version": "0.20.0-rc.1",
|
||||
"version": "0.20.0",
|
||||
"bin": "./dist/cli/index.js",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
@@ -51,7 +51,6 @@
|
||||
"@aws-sdk/client-s3": "^3.922.0",
|
||||
"@bluwy/giget-core": "^0.1.6",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@cloudflare/vite-plugin": "^1.15.3",
|
||||
"@cloudflare/vitest-pool-workers": "^0.10.4",
|
||||
"@cloudflare/workers-types": "^4.20251014.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
@@ -114,7 +113,7 @@
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "3.0.9",
|
||||
"wouter": "^3.7.1",
|
||||
"wrangler": "^4.52.1",
|
||||
"wrangler": "^4.45.4",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.19.6",
|
||||
@@ -483,11 +482,9 @@
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@0.11.0", "https://registry.npmmirror.com/@clack/prompts/-/prompts-0.11.0.tgz", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.1", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg=="],
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.11", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251106.1" }, "optionalPeers": ["workerd"] }, "sha512-se23f1D4PxKrMKOq+Stz+Yn7AJ9ITHcEecXo2Yjb+UgbUDCEBch1FXQC6hx6uT5fNA3kmX3mfzeZiUmpK1W9IQ=="],
|
||||
|
||||
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.15.3", "", { "dependencies": { "@cloudflare/unenv-preset": "2.7.11", "@remix-run/node-fetch-server": "^0.8.0", "get-port": "^7.1.0", "miniflare": "4.20251125.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.51.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-o199VPWhPoKolIW7bfdk1Vzfpa/gE+1wA+J/B8Is8MH/pAVhNQG1rVxAXjNsOAuRMwfJxRwBKDScwVVZFuUUlw=="],
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.9", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20250927.0" }, "optionalPeers": ["workerd"] }, "sha512-Drm7qlTKnvncEv+DANiQNEonq0H0LyIsoFZYJ6tJ8OhAoy5udIE8yp6BsVDYcIjcYLIybp4M7c/P7ly/56SoHg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.10.4", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^5.3.2", "miniflare": "4.20251011.2", "semver": "^7.7.1", "wrangler": "4.45.4", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-PY9mexikRXEgJIXuFtr0ZWThbuBjq7VepC2sx8LpYizOLqoZ2NsUrJW+T+nojGxYNuzjulI8D14+mFezg7MhRQ=="],
|
||||
|
||||
@@ -541,7 +538,7 @@
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="],
|
||||
|
||||
"@cypress/request": ["@cypress/request@3.0.9", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "6.14.0", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw=="],
|
||||
"@cypress/request": ["@cypress/request@3.0.10", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "~6.14.1", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ=="],
|
||||
|
||||
"@dagrejs/dagre": ["@dagrejs/dagre@1.1.8", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw=="],
|
||||
|
||||
@@ -591,8 +588,6 @@
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
@@ -803,6 +798,8 @@
|
||||
|
||||
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@plasmicapp/data-sources-context": ["@plasmicapp/data-sources-context@0.1.21", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-DF86rstDK2zs/aTbPOUTWwXiVmFG2GDxXYvztIfXUNtuCvNDJKAZK2thYaULv9bGTRitPKWlwcz0zAMjYRHavQ=="],
|
||||
@@ -967,8 +964,6 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.8.1", "", {}, "sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig=="],
|
||||
|
||||
"@rjsf/core": ["@rjsf/core@5.22.2", "", { "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", "markdown-to-jsx": "^7.4.1", "nanoid": "^3.3.7", "prop-types": "^15.8.1" }, "peerDependencies": { "@rjsf/utils": "^5.22.x", "react": "^16.14.0 || >=17" } }, "sha512-awosG7y1fcu770GtKT64L3dHj3IF7iRiDwYU6jv1TUf5/Jf2TiWhTilTMyBfAEVfSiNWvh8U8dKH69k2WqswKQ=="],
|
||||
|
||||
"@rjsf/utils": ["@rjsf/utils@5.22.0", "", { "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "react-is": "^18.2.0" }, "peerDependencies": { "react": "^16.14.0 || >=17" } }, "sha512-lM9BxMziTo4R/WG82yBJA7qNrbFY/BxYE44rwacZoHW0wFprstXAu4qJpWcQihgWYSe0SRT7SxXJtsQG+TEjSw=="],
|
||||
@@ -1221,7 +1216,7 @@
|
||||
|
||||
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
|
||||
|
||||
"@tsconfig/strictest": ["@tsconfig/strictest@2.0.7", "", {}, "sha512-lG1bXQloBVvQXasPBZSBaWbs7GNW4txZMYs7XMUVzM0s4seCbJACYBPFIRpGNVD3gC8gndK00AZON1uBrJt4Fw=="],
|
||||
"@tsconfig/strictest": ["@tsconfig/strictest@2.0.8", "", {}, "sha512-XnQ7vNz5HRN0r88GYf1J9JJjqtZPiHt2woGJOo2dYqyHGGcd6OLGqSlBB6p1j9mpzja6Oe5BoPqWmeDx6X9rLw=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
@@ -1233,7 +1228,7 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
@@ -1329,41 +1324,41 @@
|
||||
|
||||
"@ungap/with-resolvers": ["@ungap/with-resolvers@0.1.0", "", {}, "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="],
|
||||
|
||||
"@verdaccio/auth": ["@verdaccio/auth@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/loaders": "8.0.0-next-8.14", "@verdaccio/signature": "8.0.0-next-8.16", "debug": "4.4.3", "lodash": "4.17.21", "verdaccio-htpasswd": "13.0.0-next-8.24" } }, "sha512-stRp0DdTTx3p6dnh2cKOPJZOhu6sZOf8evV2fpYtADYW0UyhhZwELBXukpa5WGQ3H3rWzsXSaccra+D7tB1LgA=="],
|
||||
"@verdaccio/auth": ["@verdaccio/auth@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/loaders": "8.0.0-next-8.23", "@verdaccio/signature": "8.0.0-next-8.25", "debug": "4.4.3", "lodash": "4.17.23", "verdaccio-htpasswd": "13.0.0-next-8.33" } }, "sha512-HRIqEj9J7t95ZG3pCVpaCJoXCNVPB0R2DpkEXfG19TXsapf/mv5vMnBYG6O1C/ShNagHMA7z+Z6NwFZXHUzm9g=="],
|
||||
|
||||
"@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-TRTVY6g2bH5V/1MQOXmdwOIuiT8i/tAtRX4T7FHwCutWTMfdFLgwy4e1VvTH8d1MwGRNyVin4O8PHVu5Xh4ouA=="],
|
||||
"@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "js-yaml": "4.1.1", "lodash": "4.17.23" } }, "sha512-6/n/qcVMbNTK8oFY8l6vlJMeG6zor/aOFcR2Wd6yCoKcehITigmLn8MFziZPriYivzxYJJRjlaKO9+HuuQSKCA=="],
|
||||
|
||||
"@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.24", "", { "dependencies": { "ajv": "8.17.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "minimatch": "7.4.6", "process-warning": "1.0.0", "semver": "7.7.3" } }, "sha512-58Et0Mj562ergUd7galslWNsTseOHBCDkCIHokmoeWGX4+P46Aoa9+G0laFHyZxxfkuGkZXhO1WHysc9LzkfiA=="],
|
||||
"@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.33", "", { "dependencies": { "ajv": "8.18.0", "http-errors": "2.0.1", "http-status-codes": "2.3.0", "minimatch": "7.4.9", "process-warning": "1.0.0", "semver": "7.7.4" } }, "sha512-ndPAfZVyN677y/EZX+USkL0/aOcU5rvnzS99nRCIHarZB44WMno9jl6FdX5Ax3b3exGo9GsxhEdbYHgOYaRlLQ=="],
|
||||
|
||||
"@verdaccio/file-locking": ["@verdaccio/file-locking@10.3.1", "", { "dependencies": { "lockfile": "1.0.4" } }, "sha512-oqYLfv3Yg3mAgw9qhASBpjD50osj2AX4IwbkUtyuhhKGyoFU9eZdrbeW6tpnqUnj6yBMtAPm2eGD4BwQuX400g=="],
|
||||
|
||||
"@verdaccio/hooks": ["@verdaccio/hooks@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/logger": "8.0.0-next-8.24", "debug": "4.4.3", "got-cjs": "12.5.4", "handlebars": "4.7.8" } }, "sha512-jrBHk51rsANI47YbHCFKprBPelLDklwKhkMINEYnFOQwuB3HEcupd6hGNDaj64sRnZNoK2VuJH0cAWn0iqVErg=="],
|
||||
"@verdaccio/hooks": ["@verdaccio/hooks@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/logger": "8.0.0-next-8.33", "debug": "4.4.3", "got-cjs": "12.5.4", "handlebars": "4.7.8" } }, "sha512-8xP6kVOCufjaZWriFtaxP3HEmvYTIBhcxWldui4eCG7dzBdZzPlCkRbYKWP8LMnOEIxbJNbeFkokOaI1nho1jg=="],
|
||||
|
||||
"@verdaccio/loaders": ["@verdaccio/loaders@8.0.0-next-8.14", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "lodash": "4.17.21" } }, "sha512-cWrTTJ7HWjrzhXIVVXPHFUFTdzbRgvU5Xwte8O/JPMtrEAxtbjg18kCIdQwAcomB1S+BkffnnQJ8TPLymnuCrg=="],
|
||||
"@verdaccio/loaders": ["@verdaccio/loaders@8.0.0-next-8.23", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "lodash": "4.17.23" } }, "sha512-UruK7pFz7aRKkR41/Hg/cPFSqX5Oxbx9rKGWK5ql3mvg2+xaWleRwknmnNjbVod7QK6cWsYA6cKnttRBTQuPkQ=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy": ["@verdaccio/local-storage-legacy@11.1.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.21", "@verdaccio/file-locking": "10.3.1", "@verdaccio/streams": "10.2.1", "async": "3.2.6", "debug": "4.4.1", "lodash": "4.17.21", "lowdb": "1.0.0", "mkdirp": "1.0.4" } }, "sha512-P6ahH2W6/KqfJFKP+Eid7P134FHDLNvHa+i8KVgRVBeo2/IXb6FEANpM1mCVNvPANu0LCAmNJBOXweoUKViaoA=="],
|
||||
|
||||
"@verdaccio/logger": ["@verdaccio/logger@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/logger-commons": "8.0.0-next-8.24", "pino": "9.13.1" } }, "sha512-7/arkwQy2zI5W5z9zMf5wyWiZx18xbLultteNPWHkQv9CtoFtuK+TSY8rH7ITtCGoNCcB94jvvTYRylxAdaHEw=="],
|
||||
"@verdaccio/logger": ["@verdaccio/logger@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/logger-commons": "8.0.0-next-8.33", "pino": "9.14.0" } }, "sha512-8nR3hreOcINIbMIOohhxZNUL3l0uvgGYQecl8WJvR4XizYTp1vPHv4qz1CPyuaI5bKj1QFRqHkKvvtEnD1A0rw=="],
|
||||
|
||||
"@verdaccio/logger-commons": ["@verdaccio/logger-commons@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/logger-prettify": "8.0.0-next-8.4", "colorette": "2.0.20", "debug": "4.4.3" } }, "sha512-gEBUajG1m93xG+FJ+9+Mxg3FC9tSnP0RHyrhb4gPZPqft7JpRkwjbqpjxASGzPyxdTJZwiAefr7aafXv0xMpJA=="],
|
||||
"@verdaccio/logger-commons": ["@verdaccio/logger-commons@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/logger-prettify": "8.0.0-next-8.4", "colorette": "2.0.20", "debug": "4.4.3" } }, "sha512-WmMq/6HyRliKWus3sQR9Pgodopzbp84dl/h/E0tnxuOmzc/eDwYCEiQMfFhIBaOlpVJdsdLYqNAM9+YkoSDK0g=="],
|
||||
|
||||
"@verdaccio/logger-prettify": ["@verdaccio/logger-prettify@8.0.0-next-8.4", "", { "dependencies": { "colorette": "2.0.20", "dayjs": "1.11.13", "lodash": "4.17.21", "on-exit-leak-free": "2.1.2", "pino-abstract-transport": "1.2.0", "sonic-boom": "3.8.1" } }, "sha512-gjI/JW29fyalutn/X1PQ0iNuGvzeVWKXRmnLa7gXVKhdi4p37l/j7YZ7n44XVbbiLIKAK0pbavEg9Yr66QrYaA=="],
|
||||
|
||||
"@verdaccio/middleware": ["@verdaccio/middleware@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "debug": "4.4.3", "express": "4.21.2", "express-rate-limit": "5.5.1", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "2.6.0" } }, "sha512-LuFralbC8bxl2yQx9prKEMrNbxj8BkojcUDIa+jZZRnghaZrMm1yHqf5eLHCrBBIOM3m2thJ6Ux2UfBCQP4UKA=="],
|
||||
"@verdaccio/middleware": ["@verdaccio/middleware@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/url": "13.0.0-next-8.33", "debug": "4.4.3", "express": "4.22.1", "express-rate-limit": "5.5.1", "lodash": "4.17.23", "lru-cache": "7.18.3" } }, "sha512-iMzE3stUp/qocHzFX2+xkzUe6L89vWZ/J4a4v7IxPu6KqBH39XCg4gI8Qu0xXRIFeYSTpZNzRUO+FTkeZRhJ1A=="],
|
||||
|
||||
"@verdaccio/search-indexer": ["@verdaccio/search-indexer@8.0.0-next-8.5", "", {}, "sha512-0GC2tJKstbPg/W2PZl2yE+hoAxffD2ZWilEnEYSEo2e9UQpNIy2zg7KE/uMUq2P72Vf5EVfVzb8jdaH4KV4QeA=="],
|
||||
|
||||
"@verdaccio/signature": ["@verdaccio/signature@8.0.0-next-8.16", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "jsonwebtoken": "9.0.2" } }, "sha512-JBIpoYJQFjo3ITTRjum1IjXxNrSYcPoBLZTPlt9OLL5Brd7s1fJkTBgs9tbcCRZPrek/XBIJ/cLFZZn8zkc/bQ=="],
|
||||
"@verdaccio/signature": ["@verdaccio/signature@8.0.0-next-8.25", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "jsonwebtoken": "9.0.3" } }, "sha512-3qMHZ9uXgNmRQDw7Bz3coCbXJAfI3q+bWRXQ0/fKg03LgSV6pbMD0HinGKcIBn0Kni+e8IsXxBfrs5ga4FF+Rg=="],
|
||||
|
||||
"@verdaccio/streams": ["@verdaccio/streams@10.2.1", "", {}, "sha512-OojIG/f7UYKxC4dYX8x5ax8QhRx1b8OYUAMz82rUottCuzrssX/4nn5QE7Ank0DUSX3C9l/HPthc4d9uKRJqJQ=="],
|
||||
|
||||
"@verdaccio/tarball": ["@verdaccio/tarball@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "debug": "4.4.3", "gunzip-maybe": "1.4.2", "tar-stream": "3.1.7" } }, "sha512-rDz8gWukO7dcaWzMTr7wMvKPUsRHclVzZljyTERplpIX9NWGHRsMRO7NjoTIUWtDS0euMlRVDnR8QVnYDWl2uA=="],
|
||||
"@verdaccio/tarball": ["@verdaccio/tarball@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/url": "13.0.0-next-8.33", "debug": "4.4.3", "gunzip-maybe": "1.4.2", "tar-stream": "3.1.7" } }, "sha512-VQZ8I5Fs6INxO1cY6Lpk4Wx3sD0Uy7YIzTY9qWrZNRPGDxGcZu2fyFMUfoNc1+ygJP0r5TTqdVAY74z0iQnsWg=="],
|
||||
|
||||
"@verdaccio/ui-theme": ["@verdaccio/ui-theme@8.0.0-next-8.24", "", {}, "sha512-A8lMenzJmC0EioBjQ9+L2e8tv/iEB/jLJKH4WJjPYa8B1xlm/LLUuk2aBg7pYD1fPWuazvJiH/NAnkrA09541g=="],
|
||||
"@verdaccio/ui-theme": ["@verdaccio/ui-theme@8.0.0-next-8.30", "", {}, "sha512-MSvFnYeocTWp6hqtIqtwp7Zm920UNhO3zQkKTPT6qGNjarkhxCCWRcSOAz7oJZhpbyLXc85zuxOLkTezCl4KUg=="],
|
||||
|
||||
"@verdaccio/url": ["@verdaccio/url@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "lodash": "4.17.21", "validator": "13.15.15" } }, "sha512-zNHR9qgiDTXp+IuOtz925tzGteXGj18IZphvxo2apgz3cAeUpL/nnmp4ZScczpxWxE8sT/88xVYgJm+Ec8fNCA=="],
|
||||
"@verdaccio/url": ["@verdaccio/url@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "validator": "13.15.26" } }, "sha512-26LzQTCuUFFcNasAdzayBqsYWBheQy+D4tSWnVtmj4kKQnVhufBvHLjpEjK1gqphOMx6/Mju0IQe0LsjRc1zdg=="],
|
||||
|
||||
"@verdaccio/utils": ["@verdaccio/utils@8.1.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-2e54Z1J1+OPM0LCxjkJHgwFm8jESsCYaX6ARs3+29hjoI75uiSphxFI3Hrhr+67ho/7Mtul0oyakK6l18MN/Dg=="],
|
||||
"@verdaccio/utils": ["@verdaccio/utils@8.1.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "lodash": "4.17.23", "minimatch": "7.4.9" } }, "sha512-WLE8qTBgzuZPa+gq/nKPWtF4MTMayaeOD3Qy6yfChxNvFXY+6yPFkS0QocXL7CdB2A+w+ylgTOOzKr0tWvyTpQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.43", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew=="],
|
||||
|
||||
@@ -1423,7 +1418,7 @@
|
||||
|
||||
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
@@ -1615,7 +1610,7 @@
|
||||
|
||||
"builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -1753,7 +1748,7 @@
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@6.0.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.7.2" } }, "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg=="],
|
||||
|
||||
@@ -1937,7 +1932,7 @@
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"envinfo": ["envinfo@7.15.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g=="],
|
||||
"envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||
|
||||
@@ -2033,7 +2028,7 @@
|
||||
|
||||
"expect-type": ["expect-type@1.2.0", "", {}, "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA=="],
|
||||
|
||||
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@5.5.1", "", {}, "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="],
|
||||
|
||||
@@ -2239,7 +2234,7 @@
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
@@ -2481,7 +2476,7 @@
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
|
||||
|
||||
@@ -2521,7 +2516,7 @@
|
||||
|
||||
"jsonv-ts": ["jsonv-ts@0.10.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-IfuXZigNjLQzW4X7dLRTpwd1pD1lk86SoXBWmLdF+VE6SE4PcXevWs8c/bPl7qVrZXhh8lYwbTF7TFtgO2/jXg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="],
|
||||
|
||||
@@ -2529,9 +2524,9 @@
|
||||
|
||||
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||
|
||||
"jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="],
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
@@ -2599,7 +2594,7 @@
|
||||
|
||||
"lockfile": ["lockfile@1.0.4", "", { "dependencies": { "signal-exit": "^3.0.2" } }, "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||
|
||||
@@ -2695,7 +2690,7 @@
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
|
||||
"miniflare": ["miniflare@3.20250718.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-cW/NQPBKc+fb0FwcEu+z/v93DZd+/6q/AF0iR0VFELtNPOsCvLalq6ndO743A7wfZtFxMxvuDQUXNx3aKQhOwA=="],
|
||||
"miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="],
|
||||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
@@ -2913,7 +2908,7 @@
|
||||
|
||||
"pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
|
||||
|
||||
"pino": ["pino@9.13.1", "", { "dependencies": { "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-Szuj+ViDTjKPQYiKumGmEn3frdl+ZPSdosHyt9SnUevFosOkMY2b7ipxlEctNKPmMD/VibeBI+ZcZCJK+4DPuw=="],
|
||||
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
||||
|
||||
@@ -3009,7 +3004,7 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||
|
||||
"query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="],
|
||||
|
||||
@@ -3201,7 +3196,7 @@
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
@@ -3259,8 +3254,6 @@
|
||||
|
||||
"slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="],
|
||||
|
||||
"slow-redact": ["slow-redact@0.3.2", "", {}, "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"smtp-address-parser": ["smtp-address-parser@1.0.10", "", { "dependencies": { "nearley": "^2.20.1" } }, "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg=="],
|
||||
@@ -3441,7 +3434,7 @@
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
|
||||
|
||||
@@ -3519,7 +3512,7 @@
|
||||
|
||||
"typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
@@ -3603,15 +3596,15 @@
|
||||
|
||||
"validate.io-number": ["validate.io-number@1.0.3", "", {}, "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="],
|
||||
|
||||
"validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="],
|
||||
"validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"verdaccio": ["verdaccio@6.2.1", "", { "dependencies": { "@cypress/request": "3.0.9", "@verdaccio/auth": "8.0.0-next-8.24", "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/hooks": "8.0.0-next-8.24", "@verdaccio/loaders": "8.0.0-next-8.14", "@verdaccio/local-storage-legacy": "11.1.1", "@verdaccio/logger": "8.0.0-next-8.24", "@verdaccio/middleware": "8.0.0-next-8.24", "@verdaccio/search-indexer": "8.0.0-next-8.5", "@verdaccio/signature": "8.0.0-next-8.16", "@verdaccio/streams": "10.2.1", "@verdaccio/tarball": "13.0.0-next-8.24", "@verdaccio/ui-theme": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "@verdaccio/utils": "8.1.0-next-8.24", "JSONStream": "1.3.5", "async": "3.2.6", "clipanion": "4.0.0-rc.4", "compression": "1.8.1", "cors": "2.8.5", "debug": "4.4.3", "envinfo": "7.15.0", "express": "4.21.2", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "3.0.0", "semver": "7.7.2", "verdaccio-audit": "13.0.0-next-8.24", "verdaccio-htpasswd": "13.0.0-next-8.24" }, "bin": { "verdaccio": "bin/verdaccio" } }, "sha512-b7EjPyVKvO/7J2BtLaybQqDd8dh4uUsuQL1zQMVLsw3aYqBsHCAOa6T1zb6gpCg68cNUHluw7IjLs2hha72TZA=="],
|
||||
"verdaccio": ["verdaccio@6.3.2", "", { "dependencies": { "@cypress/request": "3.0.10", "@verdaccio/auth": "8.0.0-next-8.33", "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/hooks": "8.0.0-next-8.33", "@verdaccio/loaders": "8.0.0-next-8.23", "@verdaccio/local-storage-legacy": "11.1.1", "@verdaccio/logger": "8.0.0-next-8.33", "@verdaccio/middleware": "8.0.0-next-8.33", "@verdaccio/search-indexer": "8.0.0-next-8.5", "@verdaccio/signature": "8.0.0-next-8.25", "@verdaccio/streams": "10.2.1", "@verdaccio/tarball": "13.0.0-next-8.33", "@verdaccio/ui-theme": "8.0.0-next-8.30", "@verdaccio/url": "13.0.0-next-8.33", "@verdaccio/utils": "8.1.0-next-8.33", "JSONStream": "1.3.5", "async": "3.2.6", "clipanion": "4.0.0-rc.4", "compression": "1.8.1", "cors": "2.8.6", "debug": "4.4.3", "envinfo": "7.21.0", "express": "4.22.1", "lodash": "4.17.23", "lru-cache": "7.18.3", "mime": "3.0.0", "semver": "7.7.4", "verdaccio-audit": "13.0.0-next-8.33", "verdaccio-htpasswd": "13.0.0-next-8.33" }, "bin": { "verdaccio": "bin/verdaccio" } }, "sha512-9BmfrGlakdAW1QNBrD2GgO8hOhwIp6ogbAhaaDgtDsK3/94qXwS6n2PM1/gG2V/zFC5JH1rWbLia390i0xbodA=="],
|
||||
|
||||
"verdaccio-audit": ["verdaccio-audit@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "express": "4.21.2", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" } }, "sha512-dXqsnhTGqOuIsZq/MrW05YKwhuKg94VtL0tcYI4kcT+J+fN3gKiZ1Q3wDPaVzCMc081stBlKhi+SL66gIidHdA=="],
|
||||
"verdaccio-audit": ["verdaccio-audit@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "express": "4.22.1", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" } }, "sha512-qBMN0b4cbSbo6RbOke0QtVy/HuSzmxgRXIjnXM3C86ZhjN6DlhdeAoQYcZUfbXM8BklRXtObAnMoTlgeJ7BJLA=="],
|
||||
|
||||
"verdaccio-htpasswd": ["verdaccio-htpasswd@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/file-locking": "13.0.0-next-8.6", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "debug": "4.4.3", "http-errors": "2.0.0", "unix-crypt-td-js": "1.1.4" } }, "sha512-nZ+V/szt3lXQRIyqvOpJlW0MceLM3tUyTGwqy4y0uwq7w9KGD/VqytnCpiQbkjVmmfScimXwRW7PHjHyRXLCVw=="],
|
||||
"verdaccio-htpasswd": ["verdaccio-htpasswd@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/file-locking": "13.0.0-next-8.6", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "debug": "4.4.3", "http-errors": "2.0.1", "unix-crypt-td-js": "1.1.4" } }, "sha512-ZYqvF/EuA4W4idfv2O1ZN8YhSI2xreFbGdrcWgOd+QBedDin7NzMhgypbafutm9mPhkI6Xv/+3syee1u1cEL8g=="],
|
||||
|
||||
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
|
||||
|
||||
@@ -3675,7 +3668,7 @@
|
||||
|
||||
"wouter": ["wouter@3.7.1", "", { "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw=="],
|
||||
|
||||
"wrangler": ["wrangler@4.52.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.7.12", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20251202.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251202.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251202.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-rIzDxzPnLAaqBF+SdHGd9Az0ELEWtIBwPp5diCR58p2F4C+KgNGGpPMFswMntuViQ2RKRgGbk4jIzStJoUUfjQ=="],
|
||||
"wrangler": ["wrangler@4.45.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.9", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251011.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251011.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251011.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-niXT7B463wQi7WXIHjYK8txgWhuKQLrGmhjoR58SnPhlkq4wGjd3rFrkVyRc/O58clGTfs672BSGOph4XMoQKw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@3.0.1", "", { "dependencies": { "string-width": "^2.1.1", "strip-ansi": "^4.0.0" } }, "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ=="],
|
||||
|
||||
@@ -3853,20 +3846,14 @@
|
||||
|
||||
"@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"@bknd/plasmic/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||
"@bknd/plasmic/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare": ["miniflare@4.20251125.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251125.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-xY6deLx0Drt8GfGG2Fv0fHUocHAIG/Iv62Kl36TPfDzgq7/+DQ5gYNisxnmyISQdA/sm7kOvn2XRBncxjWYrLg=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler": ["wrangler@4.51.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251125.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251125.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251125.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-JHv+58UxM2//e4kf9ASDwg016xd/OdDNDUKW6zLQyE7Uc9ayYKX1QJ9NsYtpo4dC1dfg6rT67pf1aNK1cTzUDg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare": ["miniflare@4.20251011.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-5oAaz6lqZus4QFwzEJiNtgpjZR2TBVwBeIhOW33V4gu+l23EukpKja831tFIX2o6sOD/hqZmKZHplOrWl3YGtQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.45.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.9", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251011.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251011.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251011.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-niXT7B463wQi7WXIHjYK8txgWhuKQLrGmhjoR58SnPhlkq4wGjd3rFrkVyRc/O58clGTfs672BSGOph4XMoQKw=="],
|
||||
|
||||
"@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
@@ -3937,6 +3924,10 @@
|
||||
|
||||
"@puppeteer/browsers/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"@rjsf/core/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"@rjsf/utils/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="],
|
||||
|
||||
"@rollup/plugin-commonjs/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="],
|
||||
@@ -4103,27 +4094,27 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
|
||||
|
||||
"@verdaccio/core/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
|
||||
|
||||
"@verdaccio/core/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"@verdaccio/core/minimatch": ["minimatch@7.4.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.21", "", { "dependencies": { "ajv": "8.17.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "minimatch": "7.4.6", "process-warning": "1.0.0", "semver": "7.7.2" } }, "sha512-n3Y8cqf84cwXxUUdTTfEJc8fV55PONPKijCt2YaC0jilb5qp1ieB3d4brqTOdCdXuwkmnG2uLCiGpUd/RuSW0Q=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"@verdaccio/logger-prettify/dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
|
||||
|
||||
"@verdaccio/logger-prettify/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"@verdaccio/logger-prettify/pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="],
|
||||
|
||||
"@verdaccio/logger-prettify/sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="],
|
||||
|
||||
"@verdaccio/middleware/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||
|
||||
"@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
|
||||
"@verdaccio/utils/minimatch": ["minimatch@7.4.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="],
|
||||
|
||||
"@vitejs/plugin-react/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
@@ -4171,6 +4162,8 @@
|
||||
|
||||
"archiver-utils/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"archiver-utils/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"archiver-utils/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
@@ -4193,20 +4186,20 @@
|
||||
|
||||
"base/pascalcase": ["pascalcase@0.1.1", "", {}, "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw=="],
|
||||
|
||||
"bknd/miniflare": ["miniflare@4.20251125.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251125.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-xY6deLx0Drt8GfGG2Fv0fHUocHAIG/Iv62Kl36TPfDzgq7/+DQ5gYNisxnmyISQdA/sm7kOvn2XRBncxjWYrLg=="],
|
||||
"bknd/miniflare": ["miniflare@4.20251011.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-5oAaz6lqZus4QFwzEJiNtgpjZR2TBVwBeIhOW33V4gu+l23EukpKja831tFIX2o6sOD/hqZmKZHplOrWl3YGtQ=="],
|
||||
|
||||
"bknd-cli/@libsql/client": ["@libsql/client@0.14.0", "", { "dependencies": { "@libsql/core": "^0.14.0", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.4.4", "promise-limit": "^2.7.0" } }, "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"body-parser/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"browser-resolve/resolve": ["resolve@1.1.7", "", {}, "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||
|
||||
"cheerio/undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
|
||||
|
||||
"class-utils/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="],
|
||||
@@ -4257,6 +4250,8 @@
|
||||
|
||||
"eslint/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
|
||||
"eslint/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"eslint/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
@@ -4271,6 +4266,8 @@
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-flowtype/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
@@ -4301,13 +4298,13 @@
|
||||
|
||||
"expand-brackets/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"express/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
"express/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
@@ -4359,12 +4356,16 @@
|
||||
|
||||
"has-values/kind-of": ["kind-of@4.0.0", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw=="],
|
||||
|
||||
"http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"http-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"inquirer/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"ip-address/jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
|
||||
|
||||
"ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
@@ -4437,7 +4438,9 @@
|
||||
|
||||
"jsdom/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
"json-schema-compare/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"json-schema-merge-allof/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
@@ -4451,6 +4454,8 @@
|
||||
|
||||
"log-update/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="],
|
||||
|
||||
"lowdb/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lower-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"make-dir/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
@@ -4497,8 +4502,6 @@
|
||||
|
||||
"postcss-mixins/sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
|
||||
|
||||
"postcss-mixins/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"progress-estimator/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
@@ -4513,6 +4516,8 @@
|
||||
|
||||
"pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="],
|
||||
|
||||
"raw-body/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"react-redux/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="],
|
||||
@@ -4547,6 +4552,8 @@
|
||||
|
||||
"request/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
|
||||
|
||||
"request-promise-core/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"request-promise-native/tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
|
||||
|
||||
"resq/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
@@ -4577,6 +4584,8 @@
|
||||
|
||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"serialize-error/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||
@@ -4641,6 +4650,8 @@
|
||||
|
||||
"table/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"table/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"table/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
@@ -4649,6 +4660,10 @@
|
||||
|
||||
"test-exclude/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
|
||||
|
||||
"ts-jest/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
@@ -4667,8 +4682,6 @@
|
||||
|
||||
"tsup/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||
|
||||
"tsup/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"union-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"unset-value/has-value": ["has-value@0.3.1", "", { "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" } }, "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q=="],
|
||||
@@ -4689,6 +4702,8 @@
|
||||
|
||||
"vite/rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
|
||||
|
||||
"vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"vite-node/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"vite-node/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
||||
@@ -4717,13 +4732,9 @@
|
||||
|
||||
"wouter/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="],
|
||||
|
||||
"wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.12", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251125.0" }, "optionalPeers": ["workerd"] }, "sha512-SIBo+k58R9OyBsxF1jL6GdL7XHbzatT86c0be+UY5v5tg6TAuJ1/2QsRuC3pHgKVHile1HcJqEEORoS9hv8hNw=="],
|
||||
"wrangler/miniflare": ["miniflare@4.20251011.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-5oAaz6lqZus4QFwzEJiNtgpjZR2TBVwBeIhOW33V4gu+l23EukpKja831tFIX2o6sOD/hqZmKZHplOrWl3YGtQ=="],
|
||||
|
||||
"wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
||||
|
||||
"wrangler/miniflare": ["miniflare@4.20251202.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251202.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Pa5iBAVzzVT/yr7rcyr75ETm5IGCpdT61foGx+6jDj+vzISNfWZgEcSxWk1nlJboJumUJ10kC498hQudpdbDWg=="],
|
||||
|
||||
"wrangler/workerd": ["workerd@1.20251202.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251202.0", "@cloudflare/workerd-darwin-arm64": "1.20251202.0", "@cloudflare/workerd-linux-64": "1.20251202.0", "@cloudflare/workerd-linux-arm64": "1.20251202.0", "@cloudflare/workerd-windows-64": "1.20251202.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-p08YfrUMHkjCECNdT36r+6DpJIZX4kixbZ4n6GMUcLR5Gh18fakSCsiQrh72iOm4M9QHv/rM7P8YvCrUPWT5sg=="],
|
||||
"wrangler/workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="],
|
||||
|
||||
@@ -4765,30 +4776,14 @@
|
||||
|
||||
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
|
||||
|
||||
"@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
|
||||
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd": ["workerd@1.20251125.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251125.0", "@cloudflare/workerd-darwin-arm64": "1.20251125.0", "@cloudflare/workerd-linux-64": "1.20251125.0", "@cloudflare/workerd-linux-arm64": "1.20251125.0", "@cloudflare/workerd-windows-64": "1.20251125.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oQYfgu3UZ15HlMcEyilKD1RdielRnKSG5MA0xoi1theVs99Rop9AEFYicYCyK1R4YjYblLRYEiL1tMgEFqpReA=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd": ["workerd@1.20251125.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251125.0", "@cloudflare/workerd-darwin-arm64": "1.20251125.0", "@cloudflare/workerd-linux-64": "1.20251125.0", "@cloudflare/workerd-linux-arm64": "1.20251125.0", "@cloudflare/workerd-windows-64": "1.20251125.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oQYfgu3UZ15HlMcEyilKD1RdielRnKSG5MA0xoi1theVs99Rop9AEFYicYCyK1R4YjYblLRYEiL1tMgEFqpReA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.9", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20250927.0" }, "optionalPeers": ["workerd"] }, "sha512-Drm7qlTKnvncEv+DANiQNEonq0H0LyIsoFZYJ6tJ8OhAoy5udIE8yp6BsVDYcIjcYLIybp4M7c/P7ly/56SoHg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
@@ -4863,10 +4858,20 @@
|
||||
|
||||
"@types/yauzl/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@verdaccio/core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/@verdaccio/core/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/@verdaccio/core/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
|
||||
|
||||
"@verdaccio/local-storage-legacy/@verdaccio/core/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"@verdaccio/logger-prettify/pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@vitejs/plugin-react/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@vitejs/plugin-react/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||
@@ -4903,8 +4908,6 @@
|
||||
|
||||
"@vitest/mocker/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||
|
||||
"@vitest/mocker/vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"@vitest/snapshot/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@vitest/ui/tinyglobby/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||
@@ -4943,14 +4946,12 @@
|
||||
|
||||
"bknd/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"bknd/miniflare/workerd": ["workerd@1.20251125.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251125.0", "@cloudflare/workerd-darwin-arm64": "1.20251125.0", "@cloudflare/workerd-linux-64": "1.20251125.0", "@cloudflare/workerd-linux-arm64": "1.20251125.0", "@cloudflare/workerd-windows-64": "1.20251125.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oQYfgu3UZ15HlMcEyilKD1RdielRnKSG5MA0xoi1theVs99Rop9AEFYicYCyK1R4YjYblLRYEiL1tMgEFqpReA=="],
|
||||
"bknd/miniflare/workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="],
|
||||
|
||||
"bknd/miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"class-utils/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="],
|
||||
|
||||
"compress-commons/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
@@ -5109,6 +5110,8 @@
|
||||
|
||||
"next/sharp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"next/sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
@@ -5117,10 +5120,6 @@
|
||||
|
||||
"object-copy/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="],
|
||||
|
||||
"postcss-mixins/tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||
|
||||
"postcss-mixins/tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"progress-estimator/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
@@ -5181,10 +5180,6 @@
|
||||
|
||||
"tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"tsup/tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||
|
||||
"tsup/tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"unset-value/has-value/has-values": ["has-values@0.1.4", "", {}, "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ=="],
|
||||
|
||||
"unset-value/has-value/isobject": ["isobject@2.1.0", "", { "dependencies": { "isarray": "1.0.0" } }, "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA=="],
|
||||
@@ -5247,69 +5242,19 @@
|
||||
|
||||
"webdriverio/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
|
||||
|
||||
"wrangler/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"wrangler/miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251202.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-/uvEAWEukTWb1geHhbjGUeZqcSSSyYzp0mvoPUBl+l0ont4NVGao3fgwM0q8wtKvgoKCHSG6zcG23wj9Opj3Nw=="],
|
||||
"wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
|
||||
|
||||
"wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251202.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-f52xRvcI9cWRd6400EZStRtXiRC5XKEud7K5aFIbbUv0VeINltujFQQ9nHWtsF6g1quIXWkjhh5u01gPAYNNXA=="],
|
||||
"wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
|
||||
|
||||
"wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251202.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HYXinF5RBH7oXbsFUMmwKCj+WltpYbf5mRKUBG5v3EuPhUjSIFB84U+58pDyfBJjcynHdy3EtvTWcvh/+lcgow=="],
|
||||
"wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251011.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA=="],
|
||||
|
||||
"wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251202.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-++L02Jdoxz7hEA9qDaQjbVU1RzQS+S+eqIi22DkPe2Tgiq2M3UfNpeu+75k5L9DGRIkZPYvwMBMbcmKvQqdIIg=="],
|
||||
"wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251011.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ=="],
|
||||
|
||||
"wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251202.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gzeU6eDydTi7ib+Q9DD/c0hpXtqPucnHk2tfGU03mljPObYxzMkkPGgB5qxpksFvub3y4K0ChjqYxGJB4F+j3g=="],
|
||||
"wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251011.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg=="],
|
||||
|
||||
"wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="],
|
||||
|
||||
@@ -5325,30 +5270,6 @@
|
||||
|
||||
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"@bknd/plasmic/@types/bun/bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251125.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDIVJi8fPxBseRoEIzLiUJb0N+DXnah/ynS+Unzn58HEoKLetUWiV/T1Fhned//lo5krnToG9KRgVRs0SOOTpw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251125.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k5FQET5PXnWjeDqZUpl4Ah/Rn0bH6mjfUtTyeAy6ky7QB3AZpwIhgWQD0vOFB3OvJaK4J/K4cUtNChYXB9mY/A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251125.0", "", { "os": "linux", "cpu": "x64" }, "sha512-at6n/FomkftykWx0EqVLUZ0juUFz3ORtEPeBbW9ZZ3BQEyfVUtYfdcz/f1cN8Yyb7TE9ovF071P0mBRkx83ODw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251125.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EiRn+jrNaIs1QveabXGHFoyn3s/l02ui6Yp3nssyNhtmtgviddtt8KObBfM1jQKjXTpZlunhwdN4Bxf4jhlOMw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251125.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6fdIsSeu65g++k8Y2DKzNKs0BkoU+KKI6GAAVBOLh2vvVWWnCP1OgMdVb5JAdjDrjDT5i0GSQu0bgQ8fPsW6zw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251125.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDIVJi8fPxBseRoEIzLiUJb0N+DXnah/ynS+Unzn58HEoKLetUWiV/T1Fhned//lo5krnToG9KRgVRs0SOOTpw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251125.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k5FQET5PXnWjeDqZUpl4Ah/Rn0bH6mjfUtTyeAy6ky7QB3AZpwIhgWQD0vOFB3OvJaK4J/K4cUtNChYXB9mY/A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251125.0", "", { "os": "linux", "cpu": "x64" }, "sha512-at6n/FomkftykWx0EqVLUZ0juUFz3ORtEPeBbW9ZZ3BQEyfVUtYfdcz/f1cN8Yyb7TE9ovF071P0mBRkx83ODw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251125.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EiRn+jrNaIs1QveabXGHFoyn3s/l02ui6Yp3nssyNhtmtgviddtt8KObBfM1jQKjXTpZlunhwdN4Bxf4jhlOMw=="],
|
||||
|
||||
"@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251125.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6fdIsSeu65g++k8Y2DKzNKs0BkoU+KKI6GAAVBOLh2vvVWWnCP1OgMdVb5JAdjDrjDT5i0GSQu0bgQ8fPsW6zw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
|
||||
@@ -5361,16 +5282,6 @@
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251011.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251011.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251011.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg=="],
|
||||
|
||||
"@plasmicapp/nextjs-app-router/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"@verdaccio/logger-prettify/pino-abstract-transport/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
@@ -5423,15 +5334,15 @@
|
||||
|
||||
"bknd-cli/@libsql/client/libsql/@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw=="],
|
||||
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251125.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDIVJi8fPxBseRoEIzLiUJb0N+DXnah/ynS+Unzn58HEoKLetUWiV/T1Fhned//lo5krnToG9KRgVRs0SOOTpw=="],
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
|
||||
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251125.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k5FQET5PXnWjeDqZUpl4Ah/Rn0bH6mjfUtTyeAy6ky7QB3AZpwIhgWQD0vOFB3OvJaK4J/K4cUtNChYXB9mY/A=="],
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
|
||||
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251125.0", "", { "os": "linux", "cpu": "x64" }, "sha512-at6n/FomkftykWx0EqVLUZ0juUFz3ORtEPeBbW9ZZ3BQEyfVUtYfdcz/f1cN8Yyb7TE9ovF071P0mBRkx83ODw=="],
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251011.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA=="],
|
||||
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251125.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EiRn+jrNaIs1QveabXGHFoyn3s/l02ui6Yp3nssyNhtmtgviddtt8KObBfM1jQKjXTpZlunhwdN4Bxf4jhlOMw=="],
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251011.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ=="],
|
||||
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251125.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6fdIsSeu65g++k8Y2DKzNKs0BkoU+KKI6GAAVBOLh2vvVWWnCP1OgMdVb5JAdjDrjDT5i0GSQu0bgQ8fPsW6zw=="],
|
||||
"bknd/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251011.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg=="],
|
||||
|
||||
"bknd/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TypeTable } from "fumadocs-ui/components/type-table";
|
||||
|
||||
|
||||
bknd features an integrated Admin UI that can be used to:
|
||||
- fully manage your backend visually when run in [`db` mode](/usage/introduction/#ui-only-mode)
|
||||
- fully manage your backend visually when run in [`db` mode](/usage/setup/#ui-only-mode)
|
||||
- manage your database contents
|
||||
- manage your media contents
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ If the connection object is omitted, the app will try to use an in-memory databa
|
||||
|
||||
As configuration, you can either pass a partial configuration object or a complete one
|
||||
with a version number. The version number is used to automatically migrate the configuration up
|
||||
to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) only). The default configuration looks like this:
|
||||
to the latest version upon boot ([`db` mode](/usage/setup#ui-only-mode) only). The default configuration looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -210,6 +210,10 @@ to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) o
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="warn" title="Guard lockout warning">
|
||||
Setting `auth.guard.enabled` to `true` without first creating a user with an admin role will lock you out of the admin portal. See [Securing Your Admin Portal](/modules/auth#securing-your-admin-portal) for the required steps before enabling the Guard.
|
||||
</Callout>
|
||||
|
||||
You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -170,7 +170,7 @@ export const prerender = false;
|
||||
theme: "dark",
|
||||
logo_return_path: "/../"
|
||||
}}
|
||||
client:only
|
||||
client:only="react"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
{
|
||||
"pages": ["nextjs", "react-router", "astro", "vite"]
|
||||
"pages": [
|
||||
"nextjs",
|
||||
"react-router",
|
||||
"astro",
|
||||
"sveltekit",
|
||||
"tanstack-start",
|
||||
"vite",
|
||||
"nuxt"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
---
|
||||
title: "Nuxt"
|
||||
description: "Run bknd inside Nuxt"
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with Nuxt and bknd, create a new Nuxt project by following the [official guide](https://nuxt.com/docs/4.x/getting-started/installation), and then install bknd as a dependency:
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
|
||||
|
||||
```bash tab="npm"
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
```bash tab="pnpm"
|
||||
pnpm install bknd
|
||||
```
|
||||
|
||||
```bash tab="yarn"
|
||||
yarn add bknd
|
||||
```
|
||||
|
||||
```bash tab="bun"
|
||||
bun add bknd
|
||||
```
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Callout type="warning">
|
||||
When run with Node.js, a version of 22 (LTS) or higher is required. Please
|
||||
verify your version by running `node -v`, and
|
||||
[upgrade](https://nodejs.org/en/download/) if necessary.
|
||||
</Callout>
|
||||
|
||||
Now create a `bknd.config.ts` file in the root of your project:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type NuxtBkndConfig from "bknd/adapter/nuxt";
|
||||
import { em, entity, text, boolean } from "bknd";
|
||||
import { secureRandomString } from "bknd/utils";
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: secureRandomString(32),
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
// create some entries
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
|
||||
// and create a user
|
||||
await ctx.app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies NuxtBkndConfig;
|
||||
```
|
||||
|
||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||
|
||||
|
||||
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `NuxtBkndConfig` type extends the base config type with the following properties:
|
||||
|
||||
```typescript
|
||||
export type NuxtBkndConfig<Env = NuxtEnv> = FrameworkBkndConfig<Env>;
|
||||
```
|
||||
|
||||
## Serve the API and Admin UI
|
||||
|
||||
The Nuxt adapter uses Nuxt middleware to handle API requests and serve the Admin UI. Create a `/server/middleware/bknd.ts` file:
|
||||
|
||||
```typescript title="/server/middleware/bknd.ts"
|
||||
import { serve } from "bknd/adapter/nuxt";
|
||||
import config from "../../bknd.config";
|
||||
|
||||
const handler = serve(config, process.env);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const pathname = event.path;
|
||||
const request = toWebRequest(event);
|
||||
|
||||
if (pathname.startsWith("/api") || pathname !== "/") {
|
||||
const res = await handle(request);
|
||||
|
||||
if (res && res.status !== 404) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="success">
|
||||
You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI.
|
||||
</Callout>
|
||||
|
||||
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file:
|
||||
|
||||
```ts title="server/utils/bknd.ts"
|
||||
import { type NuxtBkndConfig, getApp as getNuxtApp } from "bknd/adapter/nuxt";
|
||||
import bkndConfig from "../../bknd.config";
|
||||
|
||||
export async function getApp<Env = NodeJS.ProcessEnv>(
|
||||
config: NuxtBkndConfig<Env>,
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return await getNuxtApp(config, args);
|
||||
}
|
||||
|
||||
export async function getApi({ headers, verify }: { verify?: boolean; headers?: Headers }) {
|
||||
const app = await getApp(bkndConfig, process.env);
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The adapter uses `process.env` to access environment variables, this works because Nuxt uses Nitro underneath and it will use polyfills for `process.env` making it platform/runtime agnostic.
|
||||
</Callout>
|
||||
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
You can use the `getApp` function to access the bknd API in your app to expose endpoints,
|
||||
Here are some examples:
|
||||
|
||||
```typescript title="server/routes/todos.post.ts"
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const { data, action } = body;
|
||||
|
||||
const api = await getApi({});
|
||||
|
||||
switch (action) {
|
||||
case 'get': {
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
|
||||
return { total: todos.body.meta.total, todos, limit };
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
return await api.data.createOne("todos", { title: data.title });
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
return await api.data.deleteOne("todos", data.id);
|
||||
}
|
||||
|
||||
case 'toggle': {
|
||||
return await api.data.updateOne("todos", data.id, { done: !data.done });
|
||||
}
|
||||
|
||||
default: {
|
||||
return { path: action };
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
This can't be done in the `server/api` directory as it will collide with the API endpoints created by the middleware. We will use [defineEventHandler](https://nuxt.com/docs/4.x/directory-structure/server) in the [server/routes](https://nuxt.com/docs/4.x/directory-structure/server) directory to create endpoints and use them in conjunction with composables to access the API safely.
|
||||
</Callout>
|
||||
|
||||
|
||||
### Using the API through composables
|
||||
|
||||
To use the API in your frontend components/pages, you can create a composable that uses the enpoints created in previous steps:
|
||||
|
||||
```typescript title="app/composables/useTodoActions.ts"
|
||||
import type { DB } from "bknd";
|
||||
|
||||
type Todo = DB["todos"];
|
||||
|
||||
export const useTodoActions = () => {
|
||||
const fetchTodos = () =>
|
||||
$fetch<{ limit: number; todos: Array<Todo>; total: number }>("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "get" },
|
||||
});
|
||||
|
||||
const createTodo = (title: string) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "create", data: { title } },
|
||||
});
|
||||
|
||||
const deleteTodo = (todo: Todo) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "delete", data: { id: todo.id } },
|
||||
});
|
||||
|
||||
const toggleTodo = (todo: Todo) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "toggle", data: todo },
|
||||
});
|
||||
|
||||
return { fetchTodos, createTodo, deleteTodo, toggleTodo };
|
||||
};
|
||||
```
|
||||
|
||||
### Usage in a page/component
|
||||
|
||||
Make a composable to fetch the todos:
|
||||
|
||||
```ts title="app/composables/useTodoActions.ts"
|
||||
import type { DB } from "bknd";
|
||||
|
||||
type Todo = DB["todos"];
|
||||
|
||||
export const useTodoActions = () => {
|
||||
const fetchTodos = () =>
|
||||
$fetch<{ limit: number; todos: Array<Todo>; total: number }>("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "get" },
|
||||
});
|
||||
|
||||
const createTodo = (title: string) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "create", data: { title } },
|
||||
});
|
||||
|
||||
const deleteTodo = (todo: Todo) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "delete", data: { id: todo.id } },
|
||||
});
|
||||
|
||||
const toggleTodo = (todo: Todo) =>
|
||||
$fetch("/todos", {
|
||||
method: "POST",
|
||||
body: { action: "toggle", data: todo },
|
||||
});
|
||||
|
||||
return { fetchTodos, createTodo, deleteTodo, toggleTodo };
|
||||
};
|
||||
```
|
||||
Then use the `useTodoActions` composable in a page:
|
||||
|
||||
```vue title="app/pages/todos.vue"
|
||||
<script lang="ts" setup>
|
||||
const { fetchTodos, createTodo, deleteTodo, toggleTodo } = useTodoActions();
|
||||
const { data:todos, execute } = await useAsyncData("todos", () => fetchTodos());
|
||||
|
||||
onMounted(() => {
|
||||
execute();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="todos"
|
||||
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
|
||||
>
|
||||
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
|
||||
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
|
||||
</div>
|
||||
<div v-if="data?.todos">
|
||||
<ul>
|
||||
<li v-for="todo in data.todos" :key="todo.id">
|
||||
{{ todo.title }}
|
||||
<button @click="toggleTodo(todo)">Toggle</button>
|
||||
<button @click="deleteTodo(todo)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
<form @submit.prevent="createTodo('New Todo')">
|
||||
<input type="text" placeholder="New Todo" />
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else className="flex flex-col gap-1">
|
||||
<p>
|
||||
No todos found.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
<Callout type="success">
|
||||
You can visit https://localhost:3000/todos to see all the todos.
|
||||
</Callout>
|
||||
|
||||
### Using authentication
|
||||
|
||||
Make a composable to fetch the user:
|
||||
|
||||
```ts title="app/composables/useUser.ts"
|
||||
import type { User } from "bknd";
|
||||
|
||||
export const useUser = () => {
|
||||
const getUser = () => $fetch("/api/auth/me") as Promise<{ user: User }>;
|
||||
return { getUser };
|
||||
};
|
||||
```
|
||||
|
||||
Then use the `useUser` composable in a page:
|
||||
|
||||
```vue title="app/pages/user.vue"
|
||||
<script lang="ts" setup>
|
||||
const { getUser } = useUser();
|
||||
const { data, status: userStatus, execute } = await useAsyncData("user", () => getUser());
|
||||
|
||||
onMounted(() => {
|
||||
execute();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="userStatus !== 'pending'"
|
||||
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
|
||||
>
|
||||
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
|
||||
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
|
||||
</div>
|
||||
<div v-if="data?.user">
|
||||
Logged in as {{ data.user.email }}.
|
||||
<a className="font-medium underline" href='/api/auth/logout'>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
<div v-else className="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.
|
||||
<a className="font-medium underline" href="/admin/auth/login">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
Sign in with:
|
||||
<b>
|
||||
<code>test@bknd.io</code>
|
||||
</b>
|
||||
/
|
||||
<b>
|
||||
<code>12345678</code>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Important Note
|
||||
|
||||
Use `external` attribute on Nuxt links, anytime you are traversing to an external route (like `/api/*` which are handled by bknd's middleware) to prevent vue router from intercepting the link.
|
||||
|
||||
```vue
|
||||
<NuxtLink external href="/admin">
|
||||
Admin
|
||||
</NuxtLink>
|
||||
```
|
||||
|
||||
<Callout type="error">
|
||||
If you don't use the `external` attribute, vue router will intercept the link and try to navigate to it, which will fail and result in a 404 error.
|
||||
</Callout>
|
||||
|
||||
Check the [Nuxt repository example](https://github.com/bknd-io/bknd/tree/main/examples/nuxt) for more implementation details.
|
||||
@@ -73,7 +73,9 @@ export type ReactRouterBkndConfig<Env = ReactRouterEnv> =
|
||||
|
||||
## Serve the API
|
||||
|
||||
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
|
||||
### Helper Functions (Optional)
|
||||
|
||||
For convenience, you can create a helper file to instantiate the bknd instance and retrieve the API. This is optional but recommended as it simplifies usage throughout your app. The examples below assume you've created this helper, but you can adjust the approach according to your needs.
|
||||
|
||||
```ts title="app/bknd.ts"
|
||||
import {
|
||||
@@ -108,7 +110,9 @@ export async function getApi(
|
||||
|
||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||
|
||||
Create a new api splat route file at `app/routes/api.$.ts`:
|
||||
### API Route
|
||||
|
||||
Create a catch-all route file at `app/routes/api.$.ts` that forwards requests to bknd:
|
||||
|
||||
```ts title="app/routes/api.$.ts"
|
||||
import { getApp } from "~/bknd";
|
||||
@@ -122,9 +126,22 @@ export const loader = handler;
|
||||
export const action = handler;
|
||||
```
|
||||
|
||||
If you're using [`@react-router/fs-routes`](https://reactrouter.com/how-to/file-route-conventions), this file will automatically be picked up as a route.
|
||||
|
||||
If you're manually defining routes in [`app/routes.ts`](https://reactrouter.com/api/framework-conventions/routes.ts), reference this file in your configuration:
|
||||
|
||||
```ts title="app/routes.ts"
|
||||
import { type RouteConfig, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
// your other routes...
|
||||
route("api/*", "./routes/api.$.ts"),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
## Enabling the Admin UI
|
||||
|
||||
Create a new splat route file at `app/routes/admin.$.tsx`:
|
||||
Create a route file at `app/routes/admin.$.tsx` to enable the bknd Admin UI for managing your data, schema, and users:
|
||||
|
||||
```tsx title="app/routes/admin.$.tsx"
|
||||
import { lazy, Suspense, useSyncExternalStore } from "react";
|
||||
@@ -165,6 +182,19 @@ export default function AdminPage() {
|
||||
}
|
||||
```
|
||||
|
||||
If you're using [`@react-router/fs-routes`](https://reactrouter.com/how-to/file-route-conventions), this file will automatically be picked up as a route.
|
||||
|
||||
If you're manually defining routes in `app/routes.ts`, reference this file in your configuration:
|
||||
|
||||
```ts title="app/routes.ts"
|
||||
import { type RouteConfig, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
// your other routes...
|
||||
route("admin/*", "./routes/admin.$.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
You can use the `getApi` helper function we've already set up to fetch and mutate:
|
||||
@@ -193,3 +223,24 @@ export default function Index() {
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Using React Hooks (Optional)
|
||||
|
||||
If you want to use bknd's client-side React hooks (like `useEntityQuery`, `useAuth`, etc.), wrap your app in the `ClientProvider` component. This is typically done in `app/root.tsx`:
|
||||
|
||||
```tsx title="app/root.tsx"
|
||||
// other imports
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
// ...
|
||||
export default function App() {
|
||||
return (
|
||||
<ClientProvider>
|
||||
<Outlet />
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
The `ClientProvider` automatically uses the same origin for API requests, which works perfectly when bknd is served from your React Router app. For more details on using React hooks, see the [React SDK documentation](/usage/react).
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: "SvelteKit"
|
||||
description: "Run bknd inside SvelteKit"
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with SvelteKit and bknd, create a new SvelteKit project by following the [official guide](https://svelte.dev/docs/kit/creating-a-project), and then install bknd as a dependency:
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
|
||||
|
||||
```bash tab="npm"
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
```bash tab="pnpm"
|
||||
pnpm install bknd
|
||||
```
|
||||
|
||||
```bash tab="yarn"
|
||||
yarn add bknd
|
||||
```
|
||||
|
||||
```bash tab="bun"
|
||||
bun add bknd
|
||||
```
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Callout type="warning">
|
||||
When run with Node.js, a version of 22 (LTS) or higher is required. Please
|
||||
verify your version by running `node -v`, and
|
||||
[upgrade](https://nodejs.org/en/download/) if necessary.
|
||||
</Callout>
|
||||
|
||||
Now create a `bknd.config.ts` file in the root of your project:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { SvelteKitBkndConfig } from "bknd/adapter/sveltekit";
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
} satisfies SvelteKitBkndConfig;
|
||||
```
|
||||
|
||||
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `SvelteKitBkndConfig` type extends the base config type with the following properties:
|
||||
|
||||
```typescript
|
||||
export type SvelteKitBkndConfig<Env> = Pick<RuntimeBkndConfig<Env>, "adminOptions">;
|
||||
```
|
||||
|
||||
## Serve the API
|
||||
|
||||
The SvelteKit adapter uses SvelteKit's hooks mechanism to handle API requests. Create a `src/hooks.server.ts` file:
|
||||
|
||||
```typescript title="src/hooks.server.ts"
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { serve } from "bknd/adapter/sveltekit";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import config from "../bknd.config";
|
||||
|
||||
const bkndHandler = serve(config, env);
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// handle bknd API requests
|
||||
const pathname = event.url.pathname;
|
||||
if (pathname.startsWith("/api/")) {
|
||||
const res = await bkndHandler(event);
|
||||
if (res.status !== 404) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
```
|
||||
|
||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||
|
||||
<Callout type="info">
|
||||
The adapter uses `$env/dynamic/private` to access environment variables, making it runtime-agnostic
|
||||
and compatible with any deployment target (Node.js, Bun, Cloudflare, etc.).
|
||||
</Callout>
|
||||
|
||||
## Enabling the Admin UI
|
||||
|
||||
The SvelteKit adapter supports serving the Admin UI statically. First, copy the required assets to your `static` folder by adding a postinstall script to your `package.json`:
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "bknd copy-assets --out static" // [!code highlight]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then update your `bknd.config.ts` to configure the admin base path:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { SvelteKitBkndConfig } from "bknd/adapter/sveltekit";
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
adminOptions: { // [!code highlight]
|
||||
adminBasepath: "/admin" // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
} satisfies SvelteKitBkndConfig;
|
||||
```
|
||||
|
||||
Finally, update your `hooks.server.ts` to also handle admin routes:
|
||||
|
||||
```typescript title="src/hooks.server.ts"
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { serve } from "bknd/adapter/sveltekit";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import config from "../bknd.config";
|
||||
|
||||
const bkndHandler = serve(config, env);
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// handle bknd API and admin requests
|
||||
const pathname = event.url.pathname;
|
||||
if (pathname.startsWith("/api/") || pathname.startsWith("/admin")) { // [!code highlight]
|
||||
const res = await bkndHandler(event);
|
||||
if (res.status !== 404) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
```
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
You can use the `getApp` function to access the bknd API in your server-side load functions:
|
||||
|
||||
```typescript title="src/routes/+page.server.ts"
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { getApp } from "bknd/adapter/sveltekit";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import config from "../../bknd.config";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const app = await getApp(config, env);
|
||||
const api = app.getApi();
|
||||
|
||||
const todos = await api.data.readMany("todos");
|
||||
|
||||
return {
|
||||
todos: todos.data ?? [],
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Then display the data in your Svelte component:
|
||||
|
||||
```svelte title="src/routes/+page.svelte"
|
||||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<h1>Todos</h1>
|
||||
<ul>
|
||||
{#each data.todos as todo (todo.id)}
|
||||
<li>{todo.title}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Using authentication
|
||||
|
||||
To use authentication in your load functions, pass the request headers to the API:
|
||||
|
||||
```typescript title="src/routes/+page.server.ts"
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { getApp } from "bknd/adapter/sveltekit";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import config from "../../bknd.config";
|
||||
|
||||
export const load: PageServerLoad = async ({ request }) => {
|
||||
const app = await getApp(config, env);
|
||||
const api = app.getApi({ headers: request.headers });
|
||||
await api.verifyAuth();
|
||||
|
||||
const todos = await api.data.readMany("todos");
|
||||
|
||||
return {
|
||||
todos: todos.data ?? [],
|
||||
user: api.getUser(),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Check the [SvelteKit repository example](https://github.com/bknd-io/bknd/tree/main/examples/sveltekit) for more implementation details.
|
||||
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: "Tanstack Start"
|
||||
description: "Run bknd inside Tanstack Start"
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with Tanstack Start and bknd, create a new Tanstack Start project by following the [official guide](https://tanstack.com/start/latest/docs/framework/react/getting-started#start-a-new-project-from-scratch), and then install bknd as a dependency:
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
|
||||
|
||||
```bash tab="npm"
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
```bash tab="pnpm"
|
||||
pnpm install bknd
|
||||
```
|
||||
|
||||
```bash tab="yarn"
|
||||
yarn add bknd
|
||||
```
|
||||
|
||||
```bash tab="bun"
|
||||
bun add bknd
|
||||
```
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Callout type="warning">
|
||||
When run with Node.js, a version of 22 (LTS) or higher is required. Please
|
||||
verify your version by running `node -v`, and
|
||||
[upgrade](https://nodejs.org/en/download/) if necessary.
|
||||
</Callout>
|
||||
|
||||
Now create a `bknd.config.ts` file in the root of your project:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { type TanstackStartConfig } from "bknd/adapter/tanstack-start";
|
||||
import { em, entity, text, boolean } from "bknd";
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "random_gibberish_please_change_this",
|
||||
// use something like `openssl rand -hex 32` for production
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
// create some entries
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
|
||||
// and create a user
|
||||
await ctx.app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies TanstackStartConfig;
|
||||
```
|
||||
|
||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||
|
||||
|
||||
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `TanstackStartConfig` type extends the base config type with the following properties:
|
||||
|
||||
```typescript
|
||||
export type TanstackStartConfig<Env = TanstackStartEnv> = FrameworkBkndConfig<Env>;
|
||||
```
|
||||
|
||||
## Serve the API
|
||||
|
||||
The Tanstack Start adapter uses Tanstack Start's hooks mechanism to handle API requests. Create a `/src/routes/api.$.ts` file:
|
||||
|
||||
```typescript title="/src/routes/api.$.ts"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import config from "../../bknd.config";
|
||||
import { serve } from "bknd/adapter/tanstack-start";
|
||||
|
||||
const handler = serve(config);
|
||||
|
||||
export const Route = createFileRoute("/api/$")({
|
||||
server: {
|
||||
handlers: {
|
||||
ANY: async ({ request }) => await handler(request),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file:
|
||||
|
||||
```ts title="src/bknd.ts"
|
||||
import config from "../bknd.config";
|
||||
import { getApp } from "bknd/adapter/tanstack-start";
|
||||
|
||||
export async function getApi({
|
||||
headers,
|
||||
verify,
|
||||
}: {
|
||||
verify?: boolean;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const app = await getApp(config, process.env);
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The adapter uses `process.env` to access environment variables, this works because Tanstack Start uses Nitro underneath and it will use polyfills for `process.env` making it platform/runtime agnostic.
|
||||
</Callout>
|
||||
|
||||
## Enabling the Admin UI
|
||||
|
||||
Create a page at /src/routes/admin.$.tsx:
|
||||
|
||||
```typescript title="/src/routes/admin.$.tsx"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useAuth } from "bknd/client";
|
||||
import "bknd/dist/styles.css";
|
||||
import { Admin } from "bknd/ui";
|
||||
|
||||
export const Route = createFileRoute("/admin/$")({
|
||||
ssr: false, // [!code highlight] "data-only" works too
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { user } = useAuth();
|
||||
return (
|
||||
<Admin
|
||||
withProvider={{ user: user }}
|
||||
config={{
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/../",
|
||||
theme: "system",
|
||||
}}
|
||||
baseUrl={import.meta.env.APP_URL}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Admin routes are expected to run on the client not using `ssr: false` will cause errors like `✘ [ERROR] No matching export in "node_modules/json-schema-library/dist/index.mjs" for import "Draft2019"` and production build might fail because of this
|
||||
</Callout>
|
||||
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
You can use the `getApp` function to access the bknd API in your app:
|
||||
These are a few examples how you can validate user and handle server-side requests using `createServerFn`.
|
||||
|
||||
```typescript title="src/routes/index.tsx"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
export const getTodo = createServerFn()
|
||||
.handler(async () => {
|
||||
const api = await getApi({});
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
|
||||
return { todos };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
ssr: false,
|
||||
component: App,
|
||||
loader: async () => {
|
||||
return await getTodo();
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { todos } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Todos</h1>
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<li key={todo.id}>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Using authentication
|
||||
|
||||
To use authentication in your app, pass the request headers to the API:
|
||||
|
||||
```typescript title="src/routes/user.tsx"
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getRequest } from "@tanstack/react-start/server";
|
||||
|
||||
export const getUser = createServerFn()
|
||||
.handler(async () => {
|
||||
const request = getRequest();
|
||||
const api = await getApi({ verify: true, headers: request.headers });
|
||||
const user = api.getUser();
|
||||
return { user };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/user")({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
return { user: await getUser() };
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { user } = Route.useLoaderData();
|
||||
return (
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
Logged in as {user.email}.{" "}
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/api/auth/logout" as string}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/admin/auth/login" as string}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
Sign in with:
|
||||
<b>
|
||||
<code>test@bknd.io</code>
|
||||
</b>
|
||||
/
|
||||
<b>
|
||||
<code>12345678</code>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Check the [Tanstack Start repository example](https://github.com/bknd-io/bknd/tree/main/examples/tanstack-start) for more implementation details.
|
||||
@@ -214,7 +214,7 @@ Now you can use the CLI with your Cloudflare resources.
|
||||
Instead, it's recommended to split this configuration into separate files, e.g. `bknd.config.ts` and `config.ts`:
|
||||
|
||||
```typescript title="config.ts"
|
||||
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
|
||||
import { d1, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default {
|
||||
app: (env) => ({
|
||||
@@ -236,4 +236,4 @@ As an additional safe guard, you have to set a `PROXY` environment variable to `
|
||||
|
||||
```bash
|
||||
PROXY=1 npx bknd types
|
||||
```
|
||||
```
|
||||
|
||||
@@ -27,6 +27,24 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you
|
||||
href="/integration/astro"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:svelte" className="text-fd-primary !size-6" />}
|
||||
title="SvelteKit"
|
||||
href="/integration/sveltekit"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
|
||||
title="Tanstack Start"
|
||||
href="/integration/tanstack-start"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:nuxt" className="text-fd-primary !size-6" />}
|
||||
title="Nuxt"
|
||||
href="/integration/nuxt"
|
||||
/>
|
||||
|
||||
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
|
||||
Create a new issue to request a guide for your framework.
|
||||
</Card>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"start",
|
||||
"motivation",
|
||||
"---Usage---",
|
||||
"./usage/introduction",
|
||||
"./usage/setup",
|
||||
"./usage/database",
|
||||
"./usage/cli",
|
||||
"./usage/sdk",
|
||||
|
||||
@@ -27,3 +27,69 @@ Authentication is essential for securing applications, and **bknd** provides a s
|
||||
- Compatible with any specification-compliant provider.
|
||||
|
||||
With a focus on flexibility and ease of integration, bknd's authentication system offers the essentials for managing secure user access in your applications.
|
||||
|
||||
---
|
||||
|
||||
## Server Module: `module.auth`
|
||||
|
||||
### `auth.changePassword([userId], [password])`
|
||||
|
||||
To change a user's password, use the `changePassword` method:
|
||||
|
||||
```ts
|
||||
await app.module.auth.changePassword(user.id, password);
|
||||
```
|
||||
|
||||
This method updates the password for the specified user.
|
||||
- `userId`: The ID of the user whose password should be changed.
|
||||
- `password`: The new password value.
|
||||
|
||||
This method throws an error if:
|
||||
- The user with the given `userId` is not found.
|
||||
- The user's authentication strategy is not `"password"` (e.g. the user registered via an OAuth provider).
|
||||
|
||||
|
||||
## Securing Your Admin Portal
|
||||
|
||||
<Callout type="warn" title="Do not enable the Guard without an admin user">
|
||||
Enabling the Guard without first creating a user with an admin role **will lock you out of the admin portal entirely**. There is no login screen that can save you — you'll need to manually edit the database to recover. Follow the checklist below before enabling the Guard.
|
||||
</Callout>
|
||||
|
||||
The **Guard** protects your admin portal and API endpoints by requiring authentication and proper permissions. Before enabling it, you must set up at least one user with full admin access.
|
||||
|
||||
### Checklist Before Enabling Guard
|
||||
|
||||
Complete these steps **in order** before turning on the Guard:
|
||||
|
||||
1. **Create an admin role** with `implicit_allow: true`
|
||||
- This grants full access to all permissions
|
||||
- Go to Auth → Roles → Create a new role
|
||||
- Enable the "Implicit Allow" toggle
|
||||
|
||||
2. **Create a user**
|
||||
- Go to Auth → Users → Create a new user
|
||||
- Set up their email and password
|
||||
|
||||
3. **Attach the admin role to the user**
|
||||
- Edit the user you just created
|
||||
- Assign the admin role to them
|
||||
|
||||
4. **Verify you can sign in**
|
||||
- Open an incognito/private browser window
|
||||
- Navigate to your app and sign in with the admin user
|
||||
- Confirm you have access
|
||||
|
||||
5. **Now enable the Guard**
|
||||
- Go to Auth → Settings
|
||||
- Enable the Guard
|
||||
|
||||
### Recovery: If You're Locked Out
|
||||
|
||||
If you enabled the Guard without setting up an admin user, you'll need to access your database directly:
|
||||
|
||||
1. Connect to your database using a database client or CLI tool
|
||||
2. Find the `__bknd` table
|
||||
3. Locate the row where `type = 'config'`
|
||||
4. In the `json` column, set `auth.guard.enabled` to `false`
|
||||
5. Restart your bknd instance
|
||||
6. Complete the checklist above, then re-enable the Guard
|
||||
|
||||
@@ -6,38 +6,36 @@ tags: ["documentation"]
|
||||
|
||||
import { Icon } from "@iconify/react";
|
||||
import { examples } from "@/app/_components/StackBlitz";
|
||||
import { SquareMousePointer, Code, Blend } from 'lucide-react';
|
||||
import { SquareMousePointer, Code, Blend, Rocket } from 'lucide-react';
|
||||
|
||||
## bknd is a lightweight batteries-included backend that embeds into your frontend app
|
||||
|
||||
Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment.
|
||||
<Callout type="warning" title="We are in beta">
|
||||
We're making great progress towards v1, but don't recommend production use yet.
|
||||
Follow along for updates on [GitHub Releases](https://github.com/bknd-io/bknd/releases) and in our [Discord community](https://discord.gg/952SFk8Tb8)
|
||||
</Callout>
|
||||
|
||||
- Instant backend with full REST API
|
||||
- Built on Web Standards for maximum compatibility
|
||||
- Multiple ready-made [integrations](/integration/introduction) (standalone, runtime, framework)
|
||||
- Official [API SDK](/usage/sdk) and [React SDK](/usage/react) with type-safety
|
||||
- [React elements](/usage/elements) for auto-configured authentication and media components
|
||||
- Built-in [MCP server](/usage/mcp/overview) for controlling your backend
|
||||
- Multiple run [modes](/usage/introduction#modes) (ui-only, code-only, hybrid)
|
||||
bknd includes full REST APIs, an admin dashboard, auth, media uploads, a [type-safe SDK](/usage/sdk), [React hooks](/usage/react), and plugins to extend it. Host it with your SSR app or as a standalone service. Built on Web Standards, it runs anywhere JavaScript runs.
|
||||
|
||||
## Preview
|
||||
Bring your [favorite frontend](./#start-with-a-frameworkruntime) and [favorite SQL database](./#use-your-favorite-sql-database), and we'll bring the ~~backend~~ bknd.
|
||||
|
||||
Here is a preview of **bknd** in StackBlitz:
|
||||
|
||||
<Card className="p-0 pb-1">
|
||||
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
|
||||
<Accordions className="m-1">
|
||||
<Accordion title="What's going on?">
|
||||
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
|
||||
|
||||
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
</Card>
|
||||
<Cards>
|
||||
<Card
|
||||
href="/motivation"
|
||||
title="Learn about why we built bknd"
|
||||
icon={<Rocket />}
|
||||
>
|
||||
Why another backend system?
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Quickstart
|
||||
|
||||
Enter the following command to spin up an instance:
|
||||
<Callout type="info" title="This demo bknd instance is for playing and learning">
|
||||
Don't worry about messing anything up in this stage since you're learning the ropes of bknd. If you want to start over, please delete the generated `data.db` database file and follow this tutorial again.
|
||||
</Callout>
|
||||
|
||||
Spin up a bknd instance via the [bknd CLI](/usage/cli):
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm','bun' ]}>
|
||||
|
||||
@@ -51,11 +49,54 @@ bunx bknd run
|
||||
|
||||
</Tabs>
|
||||
|
||||
To learn more about the CLI, check out the [Using the CLI](/usage/cli) guide.
|
||||
This creates a local `data.db` SQLite database and starts the bknd web server at http://localhost:1337.
|
||||
|
||||
By default, the admin dashboard is open and not guarded. This is intentional — bknd uses an opt-in philosophy to allow quick prototyping. Let's enable authentication and guard the dashboard to secure it.
|
||||
|
||||
1. Visit http://localhost:1337/auth/settings. Toggle "Authentication Enabled" to enable auth. Select "Update" to save.
|
||||
2. Visit http://localhost:1337/data/entity/users. Create a user by selecting "New User" and entering an email and password.
|
||||
3. Visit http://localhost:1337/auth/roles. Create an admin role by selecting "Create new". Name it "Admin" and select "Create". Then, scroll to the bottom of the page and enable "Implicit allow missing permissions?". Select "Update" to save.
|
||||
4. Visit http://localhost:1337/data/entity/users. Select on your newly created user, select the "Admin" role, and select "Update".
|
||||
5. It's time to guard your admin dashboard. Visit http://localhost:1337/settings/auth. Select "Edit". Scroll to "Guard" and enable it. Then, select "Save". _(This should log you out!)_
|
||||
6. Now, log in to your secured admin dashboard at http://localhost:1337/auth/login.
|
||||
|
||||
You did it! You've started the bknd server, created an admin user, and protected your app by enabling the guard.
|
||||
|
||||
## Modes
|
||||
|
||||
What you just experienced is **UI-only mode** — bknd's data and configuration is managed entirely via the admin dashboard. But that's not the only way to use bknd:
|
||||
|
||||
<Cards className="grid-cols-3">
|
||||
<Card title="UI-only" href="/usage/setup#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
Configure your backend and manage your data visually with the built-in Admin UI.
|
||||
</Card>
|
||||
<Card title="Code-only" href="/usage/setup#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
|
||||
Configure your backend programmatically with a Drizzle-like API, manage your data with the Admin UI.
|
||||
</Card>
|
||||
<Card title="Hybrid" href="/usage/setup#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
|
||||
Configure your backend visually while in development, use a read-only configuration in production.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
Learn more about each mode and the underlying configuration in [Setup & Modes](/usage/setup).
|
||||
|
||||
## Try bknd in the browser
|
||||
|
||||
<Card className="p-0 pb-1">
|
||||
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
|
||||
<Accordions className="m-1">
|
||||
<Accordion title="What's going on?">
|
||||
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
|
||||
|
||||
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
</Card>
|
||||
|
||||
## Start with a Framework/Runtime
|
||||
|
||||
Start by using the integration guide for these popular frameworks/runtimes. There will be more in the future, so stay tuned!
|
||||
Pick your framework or runtime to get started.
|
||||
|
||||
<Cards>
|
||||
<Card icon={<Icon icon="tabler:brand-nextjs" className="text-fd-primary !size-6" />} title="NextJS" href="/integration/nextjs" />
|
||||
@@ -103,6 +144,24 @@ Start by using the integration guide for these popular frameworks/runtimes. Ther
|
||||
href="/integration/deno"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:svelte" className="text-fd-primary !size-6" />}
|
||||
title="SvelteKit"
|
||||
href="/integration/sveltekit"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
|
||||
title="Tanstack Start"
|
||||
href="/integration/tanstack-start"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:nuxt" className="text-fd-primary !size-6" />}
|
||||
title="Nuxt"
|
||||
href="/integration/nuxt"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />}
|
||||
title="AWS Lambda"
|
||||
@@ -110,7 +169,7 @@ Start by using the integration guide for these popular frameworks/runtimes. Ther
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:vitest" className="text-fd-primary !size-6" />}
|
||||
icon={<Icon icon="simple-icons:vite" className="text-fd-primary !size-6" />}
|
||||
title="Vite"
|
||||
href="/integration/vite"
|
||||
/>
|
||||
@@ -167,19 +226,3 @@ The following databases are currently supported. Request a new integration if yo
|
||||
Create a new issue to request a new database integration.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Choose a mode
|
||||
|
||||
**bknd** supports multiple modes to suit your needs.
|
||||
|
||||
<Cards className="grid-cols-3">
|
||||
<Card title="UI-only" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
Configure your backend and manage your data visually with the built-in Admin UI.
|
||||
</Card>
|
||||
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
|
||||
Configure your backend programmatically with a Drizzle-like API, manage your data with the Admin UI.
|
||||
</Card>
|
||||
<Card title="Hybrid" href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
|
||||
Configure your backend visually while in development, use a read-only configuration in production.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -263,7 +263,7 @@ To automatically sync your secrets to a file, you may also use the [`syncSecrets
|
||||
|
||||
## Syncing the database (`sync`)
|
||||
|
||||
Sync your database can be useful when running in [`code`](/usage/introduction/#code-only-mode) mode. When you're ready to deploy, you can point to the production configuration and sync the database. Schema mutations are only applied when running with the `--force` option.
|
||||
Sync your database can be useful when running in [`code`](/usage/setup/#code-only-mode) mode. When you're ready to deploy, you can point to the production configuration and sync the database. Schema mutations are only applied when running with the `--force` option.
|
||||
|
||||
```bash
|
||||
$ npx bknd sync --help
|
||||
|
||||
@@ -310,7 +310,7 @@ const app = createApp({ connection });
|
||||
|
||||
## Data Structure
|
||||
|
||||
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/introduction#ui-only-mode), the data structure is only respected if the database is empty. If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
|
||||
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/setup#ui-only-mode), the data structure is only respected if the database is empty. If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
|
||||
|
||||
Here is a quick example:
|
||||
|
||||
@@ -506,9 +506,9 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
Note that in [`db` mode](/usage/introduction#ui-only-mode), the seed function will only be executed on app's first boot. If a configuration already exists in the database, it will not be executed.
|
||||
Note that in [`db` mode](/usage/setup#ui-only-mode), the seed function will only be executed on app's first boot. If a configuration already exists in the database, it will not be executed.
|
||||
|
||||
In [`code` mode](/usage/introduction#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
|
||||
In [`code` mode](/usage/setup#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
|
||||
|
||||
```bash
|
||||
npx bknd sync --seed --force
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
description: "Setting up bknd"
|
||||
icon: Pin
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
import { TypeTable } from "fumadocs-ui/components/type-table";
|
||||
import { SquareMousePointer, Code, Blend } from 'lucide-react';
|
||||
|
||||
There are several methods to get **bknd** up and running. You can choose between these options:
|
||||
|
||||
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
|
||||
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
|
||||
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
|
||||
native server and serves the UI assets statically from `node_modules`.
|
||||
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
|
||||
[Astro](/integration/astro) or [Remix](/integration/remix).
|
||||
|
||||
There is also a fourth option, which is running it inside a
|
||||
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
|
||||
|
||||
## Basic setup
|
||||
|
||||
Regardless of the method you choose, at the end all adapters come down to the actual
|
||||
instantiation of the `App`, which in raw looks like this:
|
||||
|
||||
```typescript
|
||||
import { createApp, type BkndConfig } from "bknd";
|
||||
|
||||
// create the app
|
||||
const config = {
|
||||
/* ... */
|
||||
} satisfies BkndConfig;
|
||||
const app = createApp(config);
|
||||
|
||||
// build the app
|
||||
await app.build();
|
||||
|
||||
// export for Web API compliant envs
|
||||
export default app;
|
||||
```
|
||||
|
||||
In Web API compliant environments, all you have to do is to default exporting the app, as it implements the `Fetch` API. In case an explicit `fetch` export is needed, you can use the `app.fetch` property.
|
||||
|
||||
```typescript
|
||||
const app = /* ... */;
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
}
|
||||
```
|
||||
|
||||
Check the integration details for your specific runtime or framework in the [integration](/integration/introduction) section.
|
||||
|
||||
## Modes
|
||||
|
||||
Main project goal is to provide a backend that can be configured visually with the built-in Admin UI. However, you may instead want to configure your backend programmatically, and define your data structure with a Drizzle-like API:
|
||||
|
||||
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
||||
<Card title="UI-only (default)" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
This is the default mode, it allows visual configuration and saves the configuration to the database. Expects you to deploy your backend separately from your frontend.
|
||||
</Card>
|
||||
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
|
||||
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
|
||||
</Card>
|
||||
<Card title={"Hybrid"} href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
|
||||
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
In the following sections, we'll cover the different modes in more detail. The configuration properties involved are the following:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
config: { /* ... */ }
|
||||
options: {
|
||||
mode: "db", // or "code"
|
||||
manager: {
|
||||
secrets: { /* ... */ },
|
||||
storeSecrets: true,
|
||||
},
|
||||
}
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
<TypeTable type={{
|
||||
config: {
|
||||
description: "The initial configuration when `mode` is `\"db\"`, and as the produced configuration when `mode` is `\"code\"`.",
|
||||
type: "object",
|
||||
properties: {
|
||||
/* ... */
|
||||
}
|
||||
},
|
||||
["options.mode"]: {
|
||||
description: "The options for the app.",
|
||||
type: '"db" | "code"',
|
||||
default: '"db"'
|
||||
},
|
||||
["options.manager.secrets"]: {
|
||||
description: "The app secrets to be provided when using `\"db\"` mode. This is required since secrets are extracted and stored separately to the database.",
|
||||
type: "object",
|
||||
properties: {
|
||||
/* ... */
|
||||
}
|
||||
},
|
||||
["options.manager.storeSecrets"]: {
|
||||
description: "Whether to store secrets in the database when using `\"db\"` mode.",
|
||||
type: "boolean",
|
||||
default: "true"
|
||||
}
|
||||
}} />
|
||||
|
||||
### UI-only mode
|
||||
|
||||
This mode is the default mode. It allows you to configure your backend visually with the built-in Admin UI. It expects that you deploy your backend separately from your frontend, and make changes there. No configuration is needed, however, if you want to provide an initial configuration, you can do so by passing a `config` object.
|
||||
|
||||
```typescript
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
// this will only be applied if the database is empty
|
||||
config: { /* ... */ },
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Note that when using the default UI-mode, the initial configuration using the `config` property will only be applied if the database is empty.
|
||||
</Callout>
|
||||
|
||||
|
||||
### Code-only mode
|
||||
|
||||
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { type BkndConfig, em, entity, text, boolean } from "bknd";
|
||||
import { secureRandomString } from "bknd/utils";
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
// example configuration
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: secureRandomString(64),
|
||||
},
|
||||
}
|
||||
},
|
||||
options: {
|
||||
// this ensures that the provided configuration is always used
|
||||
mode: "code",
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
Unlike the UI-only mode, the configuration passed to `config` is always applied. In case you make data structure changes, you may need to sync the schema to the database manually, e.g. using the [sync command](/usage/cli#syncing-the-database-sync).
|
||||
|
||||
### Hybrid mode
|
||||
|
||||
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance. It gives you the best of both worlds.
|
||||
|
||||
While in development, we set the mode to `"db"` where the configuration is stored in the database. When it's time to deploy, we export the configuration, and set the mode to `"code"`. While in `"db"` mode, the `config` property interprets the value as an initial configuration to use when the database is empty.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
// import your produced configuration
|
||||
import appConfig from "./appconfig.json" with { type: "json" };
|
||||
|
||||
export default {
|
||||
config: appConfig,
|
||||
options: {
|
||||
mode: process.env.NODE_ENV === "development" ? "db" : "code",
|
||||
manager: {
|
||||
secrets: process.env
|
||||
}
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
To keep your config, secrets and types in sync, you can either use the CLI or the plugins.
|
||||
|
||||
|
||||
| Type | Plugin | CLI Command |
|
||||
|----------------|-----------------------------------------------------------------------|----------------------------|
|
||||
| Configuration | [`syncConfig`](/extending/plugins/#syncconfig) | [`config`](/usage/cli/#getting-the-configuration-config) |
|
||||
| Secrets | [`syncSecrets`](/extending/plugins/#syncsecrets) | [`secrets`](/usage/cli/#getting-the-secrets-secrets) |
|
||||
| Types | [`syncTypes`](/extending/plugins/#synctypes) | [`types`](/usage/cli/#generating-types-types) |
|
||||
|
||||
|
||||
## Mode helpers
|
||||
|
||||
To make the setup using your preferred mode easier, there are mode helpers for [`code`](/usage/introduction#code-only-mode) and [`hybrid`](/usage/introduction#hybrid-mode) modes.
|
||||
|
||||
* built-in syncing of config, types and secrets
|
||||
* let bknd automatically sync the data schema in development
|
||||
* automatically switch modes in hybrid (from db to code) in production
|
||||
* automatically skip config validation in production to boost performance
|
||||
|
||||
To use it, you have to wrap your configuration in a mode helper, e.g. for `code` mode using the Bun adapter:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { code, type CodeMode } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
|
||||
|
||||
export default code<BunBkndConfig>({
|
||||
// some normal bun bknd config
|
||||
connection: { url: "file:data.db" },
|
||||
// ...
|
||||
// a writer is required, to sync the types
|
||||
writer,
|
||||
// (optional) mode specific config
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
// (optional) e.g. have the schema synced if !isProduction
|
||||
syncSchema: {
|
||||
force: true,
|
||||
drop: true,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Similarily, for `hybrid` mode:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { hybrid, type HybridMode } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
|
||||
|
||||
export default hybrid<BunBkndConfig>({
|
||||
// some normal bun bknd config
|
||||
connection: { url: "file:data.db" },
|
||||
// ...
|
||||
// reader/writer are required, to sync the types and config
|
||||
writer,
|
||||
reader,
|
||||
// supply secrets
|
||||
secrets: await Bun.file(".env.local").json(),
|
||||
// (optional) mode specific config
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
configFilePath: "bknd-config.json",
|
||||
// (optional) and have them automatically written if !isProduction
|
||||
syncSecrets: {
|
||||
outFile: ".env.local",
|
||||
format: "env",
|
||||
includeSecrets: true,
|
||||
},
|
||||
// (optional) also have the schema synced if !isProduction
|
||||
syncSchema: {
|
||||
force: true,
|
||||
drop: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -4,19 +4,24 @@ description: "Use the bknd SDK for React"
|
||||
icon: React
|
||||
tags: ["documentation"]
|
||||
---
|
||||
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
|
||||
There are 4 useful hooks to work with your backend:
|
||||
There are several useful hooks to work with your backend:
|
||||
|
||||
1. simple hooks which are solely based on the [API](/usage/sdk):
|
||||
- [`useApi`](#useapi)
|
||||
- [`useEntity`](#useentity)
|
||||
2. query hooks that wraps the API in [SWR](https://swr.vercel.app/):
|
||||
- [`useApiQuery`](#useapiquery)
|
||||
- [`useEntityQuery`](#useentityquery)
|
||||
1. **Simple hooks** based on the [API](/usage/sdk):
|
||||
- [`useApi`](#useapi) - Access the API instance
|
||||
- [`useAuth`](#useauth) - Authentication helpers and state
|
||||
- [`useEntity`](#useentity) - CRUD operations without caching
|
||||
2. **Query hooks** that wrap the API in [SWR](https://swr.vercel.app/):
|
||||
- [`useApiQuery`](#useapiquery) - Query any API endpoint with caching
|
||||
- [`useEntityQuery`](#useentityquery) - Entity CRUD with automatic caching
|
||||
3. **Utility hooks** for advanced use cases:
|
||||
- [`useInvalidate`](#useinvalidate) - Manual cache invalidation
|
||||
- [`useEntityMutate`](#useentitymutate) - Mutations without fetching
|
||||
|
||||
## Setup
|
||||
|
||||
In order to use them, make sure you wrap your `<App />` inside `<ClientProvider />`, so that these hooks point to your bknd instance:
|
||||
In order to use the React hooks, make sure you wrap your `<App />` inside `<ClientProvider />`. This provides the bknd API instance to all hooks in your component tree:
|
||||
|
||||
```tsx
|
||||
import { ClientProvider } from "bknd/client";
|
||||
@@ -26,21 +31,434 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
For all other examples below, we'll assume that your app is wrapped inside the `ClientProvider`.
|
||||
### ClientProvider Props
|
||||
|
||||
The `ClientProvider` accepts the following props:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
baseUrl: {
|
||||
description: 'The base URL of your bknd instance (similar to host in the API). If left blank, it points to the same origin, which is useful when bknd is served from your framework (e.g., Next.js, Astro, React Router)',
|
||||
type: 'string',
|
||||
},
|
||||
children: {
|
||||
description: 'React components that will have access to the bknd context',
|
||||
type: 'ReactNode',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
All [Api options](/usage/sdk#setup) are also supported and will be passed to the internal API instance. Common options include:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
token: {
|
||||
description: 'Authentication token for API requests',
|
||||
type: 'string',
|
||||
},
|
||||
storage: {
|
||||
description: 'Custom storage implementation for persisting tokens (defaults to localStorage in browsers)',
|
||||
type: '{ getItem, setItem, removeItem }',
|
||||
},
|
||||
onAuthStateChange: {
|
||||
description: 'Callback function triggered when authentication state changes',
|
||||
type: '(state: AuthState) => void',
|
||||
},
|
||||
fetcher: {
|
||||
description: 'Custom fetch implementation (useful for local/embedded mode)',
|
||||
type: '(input: RequestInfo, init?: RequestInit) => Promise<Response>',
|
||||
},
|
||||
credentials: {
|
||||
description: 'Request credentials mode',
|
||||
type: '"include" | "omit" | "same-origin"',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Usage Examples
|
||||
|
||||
**Using with a remote bknd instance:**
|
||||
|
||||
```tsx
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ClientProvider baseUrl="https://your-bknd-instance.com">
|
||||
{/* your app */}
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Using with an embedded bknd instance (same origin):**
|
||||
|
||||
```tsx
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
// no baseUrl needed - will use window.location.origin
|
||||
return <ClientProvider>{/* your app */}</ClientProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
**Using with custom authentication:**
|
||||
|
||||
```tsx
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ClientProvider
|
||||
baseUrl="https://your-bknd-instance.com"
|
||||
token="your-auth-token"
|
||||
onAuthStateChange={(state) => {
|
||||
console.log("Auth state changed:", state);
|
||||
}}
|
||||
>
|
||||
{/* your app */}
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For all examples below, we'll assume that your app is wrapped inside the `ClientProvider`.
|
||||
|
||||
## `useApi()`
|
||||
|
||||
To use the simple hook that returns the Api, you can use:
|
||||
Returns the [Api instance](/usage/sdk) from the `ClientProvider` context. This gives you direct access to all API methods for data, auth, media, and system operations.
|
||||
|
||||
```tsx
|
||||
import { useApi } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
export default async function App() {
|
||||
const api = useApi();
|
||||
|
||||
// access data API
|
||||
const posts = await api.data.readMany("posts");
|
||||
|
||||
// access auth API
|
||||
const user = await api.auth.me();
|
||||
|
||||
// access media API
|
||||
const files = await api.media.listFiles();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
host: {
|
||||
description: 'Optional host URL to create a new Api instance instead of using the one from context',
|
||||
type: 'string',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
See the [SDK documentation](/usage/sdk) for all available API methods and options.
|
||||
|
||||
## `useAuth()`
|
||||
|
||||
Provides authentication state and helper functions for login, register, logout, and token management. This hook automatically tracks the authentication state from the `ClientProvider` context.
|
||||
|
||||
```tsx
|
||||
import { useAuth } from "bknd/client";
|
||||
|
||||
export default function AuthComponent() {
|
||||
const { user, verified, login, logout } = useAuth();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await login({
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Welcome, {user.email}!</p>
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
baseUrl: {
|
||||
description: 'Optional base URL to use a different bknd instance',
|
||||
type: 'string',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Return Values
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
data: {
|
||||
description: 'The complete authentication state object',
|
||||
type: 'Partial<AuthState>',
|
||||
},
|
||||
user: {
|
||||
description: 'The currently authenticated user (or undefined if not authenticated)',
|
||||
type: 'SafeUser | undefined',
|
||||
},
|
||||
token: {
|
||||
description: 'The current authentication token',
|
||||
type: 'string | undefined',
|
||||
},
|
||||
verified: {
|
||||
description: 'Whether the token has been verified with the server',
|
||||
type: 'boolean',
|
||||
},
|
||||
login: {
|
||||
description: 'Login with email and password',
|
||||
type: '(data: { email: string; password: string }) => Promise<AuthResponse>',
|
||||
},
|
||||
register: {
|
||||
description: 'Register a new user with email and password',
|
||||
type: '(data: { email: string; password: string }) => Promise<AuthResponse>',
|
||||
},
|
||||
logout: {
|
||||
description: 'Logout and invalidate all cached data',
|
||||
type: '() => Promise<void>',
|
||||
},
|
||||
verify: {
|
||||
description: 'Verify the current token with the server and invalidate cache',
|
||||
type: '() => Promise<void>',
|
||||
},
|
||||
setToken: {
|
||||
description: 'Manually set the authentication token',
|
||||
type: '(token: string) => void',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Usage Notes
|
||||
|
||||
- The `login` and `register` functions automatically update the authentication state and store the token
|
||||
- The `logout` function clears the token and invalidates all SWR cache entries
|
||||
- The `verify` function checks if the current token is still valid with the server
|
||||
- Authentication state changes are automatically propagated to all components using `useAuth`
|
||||
|
||||
### Authentication Patterns
|
||||
|
||||
Depending on your deployment architecture, there are different ways to handle authentication:
|
||||
|
||||
#### 1. SPA with localStorage (Independent Deployments)
|
||||
|
||||
Use this pattern when your frontend and backend are deployed independently on different domains. The token is stored in the browser's localStorage.
|
||||
|
||||
```tsx
|
||||
import { ClientProvider, useAuth } from "bknd/client";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// setup ClientProvider with localStorage
|
||||
export default function App() {
|
||||
return (
|
||||
<ClientProvider
|
||||
baseUrl="https://your-backend.com"
|
||||
storage={window.localStorage}
|
||||
>
|
||||
<AuthComponent />
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthComponent() {
|
||||
const auth = useAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// important: verify auth on mount to check if stored token is still valid
|
||||
useEffect(() => {
|
||||
auth.verify();
|
||||
}, []);
|
||||
|
||||
if (auth.user) {
|
||||
return (
|
||||
<div>
|
||||
<p>Logged in as {auth.user.email}</p>
|
||||
<button onClick={() => auth.logout()}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await auth.login({ email, password });
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. SPA with Cookies (Same Domain)
|
||||
|
||||
Use this pattern when your frontend and backend are deployed on the same domain or when using a framework that serves both. Authentication is handled via HTTP-only cookies.
|
||||
|
||||
```tsx
|
||||
import { ClientProvider, useAuth } from "bknd/client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// setup ClientProvider with credentials included
|
||||
export default function App() {
|
||||
return (
|
||||
<ClientProvider
|
||||
baseUrl="https://your-app.com"
|
||||
credentials="include"
|
||||
>
|
||||
<InnerApp />
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerApp() {
|
||||
const auth = useAuth();
|
||||
|
||||
// important: verify auth on mount since cookies aren't readable from client-side JavaScript
|
||||
// cookies are included automatically in requests
|
||||
useEffect(() => {
|
||||
auth.verify();
|
||||
}, []);
|
||||
|
||||
if (auth.user) {
|
||||
return (
|
||||
<div>
|
||||
<p>Logged in as {auth.user.email}</p>
|
||||
{/* logout by navigating to the logout endpoint */}
|
||||
<a href="/api/auth/logout">Logout</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// option 1: programmatic login
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await auth.login({ email: "user@example.com", password: "password" });
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
);
|
||||
|
||||
// option 2: form-based login (traditional)
|
||||
return (
|
||||
<form method="post" action="/api/auth/password/login">
|
||||
<input type="email" name="email" placeholder="Email" />
|
||||
<input type="password" name="password" placeholder="Password" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- With `credentials: "include"`, cookies are automatically sent with every request
|
||||
- The logout endpoint (`/api/auth/logout`) clears the cookie and redirects back to the referrer
|
||||
- You can use either programmatic login with `auth.login()` or traditional form submission
|
||||
|
||||
#### 3. Full Stack (Embedded Mode)
|
||||
|
||||
Use this pattern when bknd is embedded in your framework (e.g., Next.js, Astro, React Router). The backend and frontend run in the same process.
|
||||
|
||||
```tsx
|
||||
// this example is not specific to any framework, but you can use it with any framework that supports server-side rendering
|
||||
|
||||
import { ClientProvider, useAuth } from "bknd/client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// setup: extract user from server-side
|
||||
// in your server-side code (e.g., Next.js loader, Astro endpoint):
|
||||
export async function loader({ request }) {
|
||||
// create API instance from your app (may be available in context)
|
||||
const api = app.getApi({ request }); // extracts credentials from request
|
||||
// or: const api = app.getApi({ headers: request.headers });
|
||||
|
||||
const user = api.getUser();
|
||||
// optionally: await api.verifyAuth();
|
||||
|
||||
return { user };
|
||||
}
|
||||
|
||||
// in your component:
|
||||
export default function App({ user }) {
|
||||
return (
|
||||
<ClientProvider user={user}>
|
||||
<InnerApp />
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerApp() {
|
||||
const auth = useAuth();
|
||||
|
||||
// optionally verify if not already verified
|
||||
useEffect(() => {
|
||||
if (!auth.verified) {
|
||||
auth.verify();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (auth.user) {
|
||||
return (
|
||||
<div>
|
||||
<p>Logged in as {auth.user.email}</p>
|
||||
{/* logout by navigating to the logout endpoint */}
|
||||
<a href="/api/auth/logout">Logout</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// use form-based authentication for full-stack apps
|
||||
return (
|
||||
<form method="post" action="/api/auth/password/login">
|
||||
<input type="email" name="email" placeholder="Email" />
|
||||
<input type="password" name="password" placeholder="Password" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- No `baseUrl` needed in `ClientProvider` - it automatically uses the same origin
|
||||
- Pass the `user` prop from server-side to avoid an initial unauthenticated state
|
||||
- Use `app.getApi({ request })` or `app.getApi({ headers })` on the server to extract credentials
|
||||
- The logout endpoint (`/api/auth/logout`) clears the session and redirects back
|
||||
- Authentication persists via cookies automatically handled by the framework
|
||||
|
||||
## `useApiQuery()`
|
||||
|
||||
This hook wraps the API class in an SWR hook for convenience. You can use any API endpoint
|
||||
@@ -61,24 +479,34 @@ export default function App() {
|
||||
|
||||
### Props
|
||||
|
||||
- `selector: (api: Api) => FetchPromise`
|
||||
<TypeTable
|
||||
type={{
|
||||
selector: {
|
||||
description: 'A selector function that provides an Api instance and expects an endpoint function to be returned',
|
||||
type: '(api: Api) => FetchPromise',
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
description: 'Optional SWR configuration with additional options',
|
||||
type: 'SWRConfiguration & { enabled?: boolean; refine?: (data: Data) => Data | any }',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
The first parameter is a selector function that provides an Api instance and expects an
|
||||
endpoint function to be returned.
|
||||
**Options properties:**
|
||||
|
||||
- `options`: optional object that inherits from `SWRConfiguration`
|
||||
|
||||
```ts
|
||||
type Options<Data> = import("swr").SWRConfiguration & {
|
||||
enabled?: boolean;
|
||||
refine?: (data: Data) => Data | any;
|
||||
};
|
||||
```
|
||||
|
||||
* `enabled`: Determines whether this hook should trigger a fetch of the data or not.
|
||||
* `refine`: Optional refinement that is called after a response from the API has been
|
||||
|
||||
received. Useful to omit irrelevant data from the response (see example below).
|
||||
<TypeTable
|
||||
type={{
|
||||
enabled: {
|
||||
description: 'Determines whether this hook should trigger a fetch of the data or not',
|
||||
type: 'boolean',
|
||||
},
|
||||
refine: {
|
||||
description: 'Optional refinement function called after a response from the API has been received. Useful to omit irrelevant data from the response',
|
||||
type: '(data: Data) => Data | any',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Using mutations
|
||||
|
||||
@@ -163,27 +591,46 @@ of entities instead of a single entry.
|
||||
|
||||
### Props
|
||||
|
||||
Following props are available when using `useEntityQuery([entity], [id?])`:
|
||||
|
||||
- `entity: string`: Specify the table name of the entity
|
||||
- `id?: number | string`: If an id given, it will fetch a single entry, otherwise a list
|
||||
<TypeTable
|
||||
type={{
|
||||
entity: {
|
||||
description: 'The table name of the entity',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
description: 'Optional ID. If provided, operations target a single entry; otherwise a list',
|
||||
type: 'number | string',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Returned actions
|
||||
|
||||
The following actions are returned from this hook:
|
||||
|
||||
- `create: (input: object)`: Create a new entry
|
||||
- `read: (query: Partial<RepoQuery> = {})`: If an id was given,
|
||||
it returns a single item, otherwise a list
|
||||
- `update: (input: object, id?: number | string)`: If an id was given, the id parameter is
|
||||
optional. Updates the given entry partially.
|
||||
- `_delete: (id?: number | string)`: If an id was given, the id parameter is
|
||||
optional. Deletes the given entry.
|
||||
<TypeTable
|
||||
type={{
|
||||
create: {
|
||||
description: 'Create a new entry',
|
||||
type: '(input: object) => Promise<Response>',
|
||||
},
|
||||
read: {
|
||||
description: 'If an id was given, returns a single item; otherwise returns a list',
|
||||
type: '(query?: RepoQueryIn) => Promise<Response>',
|
||||
},
|
||||
update: {
|
||||
description: 'Update an entry partially. If an id was given to the hook, the id parameter is optional',
|
||||
type: '(input: object, id?: number | string) => Promise<Response>',
|
||||
},
|
||||
_delete: {
|
||||
description: 'Delete an entry. If an id was given to the hook, the id parameter is optional',
|
||||
type: '(id?: number | string) => Promise<Response>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## `useEntityQuery()`
|
||||
|
||||
This hook wraps the actions from `useEntity` around `SWR`. The previous example would look like
|
||||
this:
|
||||
This hook wraps the actions from `useEntity` around `SWR` for automatic data fetching, caching, and revalidation. It combines the power of SWR with CRUD operations for your entities.
|
||||
|
||||
```tsx
|
||||
import { useEntityQuery } from "bknd/client";
|
||||
@@ -195,10 +642,207 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The returned CRUD actions are typed differently based on whether you provide an `id`:
|
||||
|
||||
- **With `id`** (single item mode): `update` and `_delete` don't require an `id` parameter since the item is already specified
|
||||
- **Without `id`** (list mode): `update` and `_delete` require an `id` parameter to specify which item to modify
|
||||
|
||||
```tsx
|
||||
// single item mode - id is already specified
|
||||
const { data, update, _delete } = useEntityQuery("comments", 1);
|
||||
await update({ content: "new text" }); // no id needed
|
||||
await _delete(); // no id needed
|
||||
|
||||
// list mode - must specify which item to update/delete
|
||||
const { data, update, _delete } = useEntityQuery("comments");
|
||||
await update({ content: "new text" }, 1); // id required
|
||||
await _delete(1); // id required
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
entity: {
|
||||
description: 'The table name of the entity',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
description: 'Optional ID. If provided, fetches a single entry; otherwise fetches a list',
|
||||
type: 'number | string',
|
||||
},
|
||||
query: {
|
||||
description: 'Optional query parameters for filtering, sorting, pagination, etc.',
|
||||
type: 'RepoQueryIn',
|
||||
},
|
||||
options: {
|
||||
description: 'Optional SWR configuration plus additional options',
|
||||
type: 'SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
The `query` parameter accepts a `RepoQueryIn` object with the following options:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
limit: {
|
||||
description: 'Limit the number of results',
|
||||
type: 'number',
|
||||
default: '10',
|
||||
},
|
||||
offset: {
|
||||
description: 'Skip a number of results for pagination',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
},
|
||||
sort: {
|
||||
description: 'Sort by field(s). Prefix with - for descending order (e.g., "-id" or ["name", "-createdAt"])',
|
||||
type: 'string | string[]',
|
||||
default: 'id',
|
||||
},
|
||||
where: {
|
||||
description: 'Filter conditions using query operators (e.g., { status: "active", views: { $gt: 100 } })',
|
||||
type: 'object',
|
||||
},
|
||||
with: {
|
||||
description: 'Include related entities',
|
||||
type: 'string[]',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
#### Options
|
||||
|
||||
The `options` parameter extends SWR's configuration and adds bknd-specific options:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
enabled: {
|
||||
description: 'If false, prevents the query from running (useful for conditional fetching)',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
},
|
||||
revalidateOnMutate: {
|
||||
description: "If false, mutations won't automatically trigger revalidation",
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
},
|
||||
keepPreviousData: {
|
||||
description: 'Keeps showing previous data while fetching new data',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
},
|
||||
revalidateOnFocus: {
|
||||
description: 'Controls whether to revalidate when window regains focus',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
All standard [SWR configuration options](https://swr.vercel.app/docs/api) are also supported.
|
||||
|
||||
### Return Values
|
||||
|
||||
The hook returns an object with the following properties:
|
||||
|
||||
**SWR Properties:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
data: {
|
||||
description: 'The fetched data (single entity or array of entities)',
|
||||
type: 'Entity | Entity[]',
|
||||
},
|
||||
error: {
|
||||
description: 'Error object if the request failed',
|
||||
type: 'Error',
|
||||
},
|
||||
isLoading: {
|
||||
description: 'True when the initial request is in progress',
|
||||
type: 'boolean',
|
||||
},
|
||||
isValidating: {
|
||||
description: 'True when a request or revalidation is in progress',
|
||||
type: 'boolean',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**CRUD Actions (auto-wrapped with cache revalidation):**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
create: {
|
||||
description: 'Create a new entry',
|
||||
type: '(input: object) => Promise<Response>',
|
||||
},
|
||||
update: {
|
||||
description: 'Update an entry. When id is provided to the hook (single item mode), the id parameter is optional and defaults to the hook id. When no id is provided to the hook (list mode), the id parameter is required',
|
||||
type: '(input: object, id?: number | string) => Promise<Response>',
|
||||
},
|
||||
_delete: {
|
||||
description: 'Delete an entry. When id is provided to the hook (single item mode), the id parameter is optional and defaults to the hook id. When no id is provided to the hook (list mode), the id parameter is required',
|
||||
type: '(id?: number | string) => Promise<Response>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**Cache Management:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
mutate: {
|
||||
description: 'Manually invalidate and revalidate cache for this entity',
|
||||
type: '(id?: number | string) => Promise<void>',
|
||||
},
|
||||
mutateRaw: {
|
||||
description: "Direct access to SWR's mutate function for advanced use cases",
|
||||
type: 'SWRResponse["mutate"]',
|
||||
},
|
||||
key: {
|
||||
description: 'The SWR cache key being used',
|
||||
type: 'string',
|
||||
},
|
||||
api: {
|
||||
description: 'Direct access to the data API instance',
|
||||
type: 'Api["data"]',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Query Example
|
||||
|
||||
Fetching a limited, sorted list of entities:
|
||||
|
||||
```tsx
|
||||
import { useEntityQuery } from "bknd/client";
|
||||
|
||||
export default function TodoList() {
|
||||
const { data: todos, isLoading } = useEntityQuery("todos", undefined, {
|
||||
limit: 5,
|
||||
sort: "-id", // descending by id
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map((todo) => (
|
||||
<li key={todo.id}>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Using mutations
|
||||
|
||||
All actions returned from `useEntityQuery` are conveniently wrapped around the `mutate` function,
|
||||
so you don't have think about this:
|
||||
All actions returned from `useEntityQuery` are conveniently wrapped to automatically revalidate the cache after mutations:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -239,3 +883,195 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Complete CRUD Example
|
||||
|
||||
Here's a comprehensive example showing all CRUD operations with query parameters:
|
||||
|
||||
```tsx
|
||||
import { useEntityQuery } from "bknd/client";
|
||||
|
||||
export default function TodoList() {
|
||||
const { data: todos, create, update, _delete, isLoading } = useEntityQuery(
|
||||
"todos",
|
||||
undefined, // no id, so we fetch a list
|
||||
{
|
||||
limit: 10,
|
||||
sort: "-id", // newest first
|
||||
}
|
||||
);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
action={async (formData: FormData) => {
|
||||
const title = formData.get("title") as string;
|
||||
await create({ title, done: false });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="title" placeholder="New todo" />
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
|
||||
<ul>
|
||||
{todos?.map((todo) => (
|
||||
<li key={todo.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!todo.done}
|
||||
onChange={async () => {
|
||||
await update({ done: !todo.done }, todo.id);
|
||||
}}
|
||||
/>
|
||||
<span>{todo.title}</span>
|
||||
<button onClick={() => _delete(todo.id)}>Delete</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Mutation Behavior
|
||||
|
||||
**Important notes about mutations:**
|
||||
|
||||
- **Auto-revalidation**: By default, all mutations (`create`, `update`, `_delete`) automatically revalidate all queries for that entity. This ensures your UI stays in sync.
|
||||
|
||||
- **Optimistic updates**: For more advanced scenarios, you can use `mutateRaw` to implement optimistic updates manually.
|
||||
|
||||
- **Disable auto-revalidation**: If you need more control, set `revalidateOnMutate: false`:
|
||||
|
||||
```tsx
|
||||
const { data, update } = useEntityQuery("comments", 1, undefined, {
|
||||
revalidateOnMutate: false,
|
||||
});
|
||||
|
||||
// now update won't trigger automatic revalidation
|
||||
await update({ content: "new text" });
|
||||
```
|
||||
|
||||
- **Manual revalidation**: Use the returned `mutate` function to manually trigger revalidation:
|
||||
|
||||
```tsx
|
||||
const { mutate } = useEntityQuery("comments");
|
||||
|
||||
// revalidate all "comments" queries
|
||||
await mutate();
|
||||
|
||||
// revalidate specific comment
|
||||
await mutate(commentId);
|
||||
```
|
||||
|
||||
## Utility Hooks
|
||||
|
||||
### `useInvalidate()`
|
||||
|
||||
This hook provides a convenient way to invalidate SWR cache entries for manual revalidation.
|
||||
|
||||
```tsx
|
||||
import { useInvalidate } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
const handleRefresh = async () => {
|
||||
// invalidate by string key prefix
|
||||
await invalidate("/data/comments");
|
||||
|
||||
// or invalidate using API selector
|
||||
await invalidate((api) => api.data.readMany("comments"));
|
||||
};
|
||||
|
||||
return <button onClick={handleRefresh}>Refresh Comments</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
options: {
|
||||
description: 'Configuration options',
|
||||
type: '{ exact?: boolean }',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**Options properties:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
exact: {
|
||||
description: 'If true, only invalidates the exact key match instead of keys that start with the given prefix',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `useEntityMutate()`
|
||||
|
||||
This hook provides mutation actions without fetching data. Useful when you only need to perform CRUD operations without subscribing to data updates.
|
||||
|
||||
```tsx
|
||||
import { useEntityMutate } from "bknd/client";
|
||||
|
||||
export default function QuickActions() {
|
||||
const { create, update, _delete, mutate } = useEntityMutate("todos");
|
||||
|
||||
const createTodo = async () => {
|
||||
await create({ title: "New todo", done: false });
|
||||
// manually update cache
|
||||
await mutate();
|
||||
};
|
||||
|
||||
return <button onClick={createTodo}>Quick Add Todo</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
entity: {
|
||||
description: 'The table name of the entity',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
description: 'Optional ID for single entity operations',
|
||||
type: 'number | string',
|
||||
},
|
||||
options: {
|
||||
description: 'Optional SWR configuration',
|
||||
type: 'SWRConfiguration',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**Return Values:**
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
create: {
|
||||
description: 'Create a new entry',
|
||||
type: '(input: object) => Promise<Response>',
|
||||
},
|
||||
update: {
|
||||
description: 'Update an entry',
|
||||
type: '(input: object, id?: number | string) => Promise<Response>',
|
||||
},
|
||||
_delete: {
|
||||
description: 'Delete an entry',
|
||||
type: '(id?: number | string) => Promise<Response>',
|
||||
},
|
||||
mutate: {
|
||||
description: 'Function to update cache with partial data without refetching',
|
||||
type: '(id: number | string, data: Partial<Entity>) => Promise<void>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -137,6 +137,35 @@ To retrieve a single record from an entity, use the `readOne` method:
|
||||
const { data } = await api.data.readOne("posts", 1);
|
||||
```
|
||||
|
||||
### `data.readOneBy([entity], [query])`
|
||||
|
||||
To retrieve a single record from an entity using a query (e.g., by a specific field value) instead of an ID, use the `readOneBy` method:
|
||||
|
||||
```ts
|
||||
const { data } = await api.data.readOneBy("posts", {
|
||||
where: {
|
||||
slug: "hello-world",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This is useful when you want to find a record by a unique field other than the primary key. You can also use `select`, `with`, and `join` options:
|
||||
|
||||
```ts
|
||||
const { data } = await api.data.readOneBy("users", {
|
||||
select: ["id", "email", "name"],
|
||||
where: {
|
||||
email: "user@example.com",
|
||||
},
|
||||
with: {
|
||||
posts: {
|
||||
limit: 5,
|
||||
},
|
||||
},
|
||||
join: ["profile"],
|
||||
});
|
||||
```
|
||||
|
||||
### `data.createOne([entity], [data])`
|
||||
|
||||
To create a single record of an entity, use the `createOne` method:
|
||||
@@ -266,6 +295,16 @@ const { data } = await api.auth.register("password", {
|
||||
});
|
||||
```
|
||||
|
||||
### `auth.logout()`
|
||||
|
||||
To log out the current user and clear the stored token, use the `logout` method:
|
||||
|
||||
```ts
|
||||
await api.auth.logout();
|
||||
```
|
||||
|
||||
This method takes no parameters. It sends a request to the logout endpoint and clears the authentication token. Returns a `Promise<void>`.
|
||||
|
||||
### `auth.me()`
|
||||
|
||||
To retrieve the current user, use the `me` method:
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "Setup & Modes"
|
||||
description: "Choose a mode and get bknd running with your app"
|
||||
icon: Pin
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
import { SquareMousePointer, Code, Blend } from 'lucide-react';
|
||||
|
||||
bknd supports three modes. Each mode determines how your backend is configured and where that configuration lives.
|
||||
|
||||
## Choose your mode
|
||||
|
||||
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
||||
<Card title="UI-only (default)" href="#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
Configure visually via the Admin UI. Config is stored in the database.
|
||||
</Card>
|
||||
<Card title="Code-only" href="#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
|
||||
Define your schema in code with a Drizzle-like API. Config lives in your codebase.
|
||||
</Card>
|
||||
<Card title="Hybrid" href="#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
|
||||
Use the Admin UI in development, export to code for production.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
**Not sure which to pick?**
|
||||
|
||||
- **UI-only** is best for prototyping and small projects. No code needed — configure everything through the dashboard.
|
||||
- **Code-only** is best for teams, CI/CD, and version-controlled schemas. You define your data structure programmatically.
|
||||
- **Hybrid** gives you the best of both — visual configuration while developing, locked-down code config in production.
|
||||
|
||||
You can always change modes later. Start with UI-only if you're exploring.
|
||||
|
||||
---
|
||||
|
||||
## UI-only mode
|
||||
|
||||
This is the default. Run bknd and configure everything through the Admin UI. No setup code required beyond a database connection.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
If you want to provide an initial data structure (entities, auth settings, etc.), pass it via `config`. It will only be applied when the database is empty.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
config: {
|
||||
auth: { enabled: true },
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
In UI-only mode, the `config` property is only applied on first boot. After that, all changes are made through the Admin UI.
|
||||
</Callout>
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Code-only mode
|
||||
|
||||
Define your data structure programmatically with a Drizzle-like API. The Admin UI becomes read-only for configuration — you still use it to manage data.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { type BkndConfig, em, entity, text, boolean } from "bknd";
|
||||
import { secureRandomString } from "bknd/utils";
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: { secret: secureRandomString(64) },
|
||||
},
|
||||
},
|
||||
options: {
|
||||
mode: "code",
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
Unlike UI-only mode, the `config` is applied on every boot. If you change the schema, you may need to [sync the database](/usage/cli#syncing-the-database-sync).
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Hybrid mode
|
||||
|
||||
Use the Admin UI to configure your backend while developing. When you're ready to deploy, export the config and run in code mode for production.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
import appConfig from "./appconfig.json" with { type: "json" };
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
config: appConfig,
|
||||
options: {
|
||||
mode: process.env.NODE_ENV === "development" ? "db" : "code",
|
||||
manager: {
|
||||
secrets: process.env,
|
||||
},
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
To export your config, secrets, and types, use the CLI or plugins:
|
||||
|
||||
| What | CLI Command | Plugin |
|
||||
|------|-------------|--------|
|
||||
| Configuration | [`bknd config`](/usage/cli/#getting-the-configuration-config) | [`syncConfig`](/extending/plugins/#syncconfig) |
|
||||
| Secrets | [`bknd secrets`](/usage/cli/#getting-the-secrets-secrets) | [`syncSecrets`](/extending/plugins/#syncsecrets) |
|
||||
| Types | [`bknd types`](/usage/cli/#generating-types-types) | [`syncTypes`](/extending/plugins/#synctypes) |
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Mode helpers
|
||||
|
||||
For code and hybrid modes, bknd provides helper functions that handle syncing, mode switching, and schema validation automatically.
|
||||
|
||||
```typescript title="bknd.config.ts (code mode with Bun)"
|
||||
import { code } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
|
||||
|
||||
export default code<BunBkndConfig>({
|
||||
connection: { url: "file:data.db" },
|
||||
writer,
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
});
|
||||
```
|
||||
|
||||
```typescript title="bknd.config.ts (hybrid mode with Bun)"
|
||||
import { hybrid } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
|
||||
|
||||
export default hybrid<BunBkndConfig>({
|
||||
connection: { url: "file:data.db" },
|
||||
writer,
|
||||
reader,
|
||||
secrets: await Bun.file(".env.local").json(),
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
configFilePath: "bknd-config.json",
|
||||
});
|
||||
```
|
||||
|
||||
Mode helpers give you:
|
||||
- Built-in syncing of config, types, and secrets
|
||||
- Automatic schema syncing in development
|
||||
- Automatic mode switching (db → code) in production for hybrid
|
||||
- Skipped config validation in production for faster boot
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Framework & runtime integrations](/integration/introduction) — wire bknd into Next.js, Astro, Bun, Cloudflare, etc.
|
||||
- [Database configuration](/usage/database) — choose and configure your SQL database
|
||||
- [Configuration reference](/extending/config) — full `BkndConfig` API, plugins, events, and lifecycle hooks
|
||||
@@ -9,3 +9,7 @@ tags: ["guide"]
|
||||
[Discord](https://discord.gg/952SFk8Tb8).
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn" title="Set up permissions before enabling the Guard">
|
||||
If you enable the Guard without first creating an admin role (with `implicit_allow: true`) and attaching it to a user, you will be locked out of the admin portal. See [Securing Your Admin Portal](/modules/auth#securing-your-admin-portal) for the full checklist.
|
||||
</Callout>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user