WAOMGQWTJFQWAXZT6ER6LBCZU67JRBI2RYVCHXAOPQJZSRVYAL2QC
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit(), tailwindcss()],
server: {
fs: {
allow: ['open']
},
hmr: {
protocol: 'ws',
port: 5173
},
proxy: {
'/api/': {
target: 'https://127.0.0.1:8001',
secure: false
},
'/register': {
target: 'https://127.0.0.1:8001',
secure: false
},
'/login': {
target: 'https://127.0.0.1:8001',
secure: false
}
}
}
};
export default config;
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
}
module.exports = {
plugins: [require('daisyui')],
daisyui: {
themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
darkTheme: 'dark', // name of one of the included themes for dark mode
base: true, // applies background color and foreground color for root element by default
styled: true, // include daisyUI colors and design decisions for all components
utils: true, // adds responsive and modifier utility classes
prefix: '', // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
themeRoot: ':root' // The element that receives theme color CSS variables
},
theme: {
container: {
center: true,
},
},
}
import adapter from '@sveltejs/adapter-node';
import { mdsvex } from 'mdsvex';
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [mdsvex()],
kit: {
adapter: adapter(),
paths: {
relative: false
}
}
};
export default config;
---
title: Pijul Restricted Use Policy
description: It is not okay to use Pijul products for these restricted purposes.
---
# Use Restrictions
*Last updated: May 21st, 2021*
We are proud to give our users a better way to work. We also recognise that however good the maker’s intentions, technology can amplify the ability to cause great harm. That’s why we’ve established this policy. We feel an ethical obligation to counter such harm: both in terms of dealing with instances where Pijul is used (and abused) to further such harm, and to state unequivocally that the products we make at Pijul are not safe havens for people who wish to commit such harm. If you have an account with any of our products, you can’t use them for any of the restricted purposes listed below. If we find out you are, we will take action.
This policy and process applies to any product created and owned by Pijul OÜ.
## Restricted purposes
* **Violence, or threats thereof**: If an activity qualifies as violent crime in the European Union or where you live, you may not use Pijul products to plan, perpetrate, or threaten that activity.
* **Child exploitation, sexualisation, or abuse**: We don’t tolerate any activities that create, disseminate, or otherwise cause child abuse. Keep away and stop. Just stop.
* **Hate speech**: You cannot use our products to advocate for the extermination, domination, or oppression of people.
* **Harassment**: Intimidating or targeting people or groups through repeated communication, including using racial slurs or dehumanising language, is not welcome at Pijul.
* **Doxing**: If you are using Pijul products to share other peoples’ private personal information for the purposes of harassment, we don’t want anything to do with you.
* **Malware or spyware**: Code for good, not evil. If you are using our products to make or distribute anything that qualifies as malware or spyware — including remote user surveillance — begone.
* **Phishing or otherwise attempting fraud**: It is not okay to lie about who you are or who you affiliate with to steal from, extort, or otherwise harm others.
* **Cybersquatting**: We don’t like username extortionists. If you purchase a Pijul product account in someone else’s name and then try to sell that account to them, you are [cybersquatting](https://www.law.cornell.edu/uscode/text/15/1125). Cybersquatting accounts are subject to immediate cancellation.
* **Infringing on intellectual property**: You can’t use Pijul products to make or disseminate work that uses the intellectual property of others beyond what is permitted by [European copyright law](https://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=CELEX:32001L0029:EN:HTML).
While our use restrictions are comprehensive, they can’t be exhaustive — it’s possible an offence could defy categorisation, present for the first time, or illuminate a moral quandary we hadn’t yet considered. That said, we hope the overarching spirit is clear: our products are not to be harnessed for harm, whether mental, physical, personal or civic. Different points of view — philosophical, religious, and political — are welcome, but ideologies like white nationalism, or hate-fuelled movements anchored by oppression, violence, abuse, extermination, or domination of one group over another, will not be accepted here.
## How to report abuse
For cases of suspected malware, spyware, phishing, spamming, and cybersquatting, please alert us at [abuse@pijul.org](mailto:abuse@pijul.org).
For all other cases, please let us know by emailing [report@pijul.org](mailto:report@pijul.org). If you’re not 100% sure if something rises to the level of our use restrictions policy, report it anyway.
Please share as much as you are comfortable with about the account, the content or behaviour you are reporting, and how you found it. Sending us a URL or screenshots is super helpful. If you need a secure file transfer, let us know and we will send you a link. We will not disclose your identity to anyone associated with the reported account.
Someone on our team will respond as soon as possible to let you know we’ve begun investigating. We will also let you know the outcome of our investigation (unless you ask us not to, or we are not allowed to under law).
Adapted from the [Basecamp open-source policies](https://github.com/basecamp/policies) / [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
<style>
h1 { font-size: 30pt; }
h2 { font-size: 20pt; }
</style>
---
title: Terms of Service
description: All the terms that you agree to when you sign up for a Pijul product.
---
# Terms of Service
*Last updated: May 21st, 2023*
From everyone at Pijul, thank you for using our products! We build them to help you do your best work. Because we don’t know every one of our users personally, we have to put in place some Terms of Service to help keep the ship afloat.
When we say “Company”, “we”, “our”, or “us” in this document, we are referring to [Pijul OÜ](https://pijul.org).
When we say “Services”, we mean our websites, including pijul.org, pijul.com, nest.pijul.org and nest.pijul.com, and any product created and maintained by Pijul OÜ. That includes Pijul (all versions), and The Nest (all versions), whether delivered within a web browser, desktop application, or another format.
When we say “You” or “your”, we are referring to the people or organisations that own an account with one or more of our Services.
We may update these Terms of Service ("Terms") in the future. Whenever we make a significant change to our policies, we will refresh the date at the top of this page and take any other appropriate steps to notify account holders.
When you use our Services, now or in the future, you are agreeing to the latest Terms. There may be times where we do not exercise or enforce a right or provision of the Terms; however, that does not mean we are waiving that right or provision. **These Terms do contain a limitation of our liability.**
If you violate any of the Terms, we may terminate your account. That’s a broad statement and it means you need to place a lot of trust in us. We do our best to deserve that trust, for example by keeping an open door to [your feedback](mailto:contact@pijul.org).
## Account Terms
1. You are responsible for maintaining the security of your account, password and cryptographic signing keys. The Company cannot and will not be liable for any loss or damage from your failure to comply with this security obligation.
<!--We recommend all users set up [two-factor authentication](https://m.signalvnoise.com/basecamp-no-longer-requires-google-for-two-factor-authentication/) for added security. In some of our Services, we may require it.-->
2. You may not use the Services for any purpose outlined in our [Use Restrictions policy](../terms/abuse), and you may not permit any of your users to do so, either.
3. You are responsible for all content posted to and activity that occurs under your account, including content posted by and activity of any users in your account.
4. You must be a human. Accounts registered by “bots” or other automated methods are not permitted.
## Payment, Refunds, and Plan Changes
1. If you are using a free version of one of the Nest, it is really free: we do not ask you for your credit card and — just like for customers who pay for our Services — we do not sell your data.
<!--2. For paid Services that offer a free trial, we explain the length of trial when you sign up. After the trial period, you need to pay in advance to keep using the Service. If you do not pay, we will freeze your account and it will be inaccessible until you make payment. If your account has been frozen for a while, we will queue it up for auto-cancellation. See our [Cancellation policy](../cancellation/index.md) for more details.-->
3. If you are upgrading from a free plan to a paid plan, we will charge your card immediately and your billing cycle starts on the day of upgrade. For other upgrades or downgrades in plan level, the new rate starts from the next billing cycle.
4. All fees are exclusive of all taxes, levies, or duties imposed by taxing authorities. Where required, we will collect those taxes on behalf of the taxing authority and remit those taxes to taxing authorities. Otherwise, you are responsible for payment of all taxes, levies, or duties.
5. We process refunds on a case-by-case basis and do our best to be as fair as possible. If you believe you've been incorrectly charged, please contact [contact@pijul.org](mailto:contact@pijul.org).
## Cancellation and Termination
1. You are solely responsible for properly cancelling your account. Within each of our Services, we provide a simple no-questions-asked cancellation link.
2. All of your content will be inaccessible from the Services immediately upon account cancellation. Within 60 days, all content will be permanently deleted from active systems and logs, with the exception of the content necessary for the proper functioning of our services, such as comments made to another repository, which will be pseudonymised upon account termination.
3. If you cancel the Service before the end of your current paid up month, your cancellation will take effect immediately, and you will not be charged again. We do not automatically prorate unused time in the last billing cycle.
4. We have the right to suspend or terminate your account and refuse any and all current or future use of our Services for any reason at any time. Suspension means you and any other users on your account will not be able to access the account or any content in the account. Termination will furthermore result in the deletion of your account or your access to your account, and the forfeiture and relinquishment of all content in your account. We also reserve the right to refuse the use of the Services to anyone for any reason at any time. We have this clause because statistically speaking, out of the hundreds of thousands of accounts on our Services, there is at least one doing something nefarious. There are some things we staunchly stand against and this clause is how we exercise that stance. For more details, see our [Use Restrictions policy](../terms/abuse).
5. Verbal, physical, written or other abuse (including threats of abuse or retribution) of a Company employee or officer will result in immediate account termination.
## Modifications to the Service and Prices
1. We make a promise to our customers to support our Services [until the end of the Internet](../until-the-end-of-the-internet/index.md). That means when it comes to security, privacy, and customer support, we will continue to maintain any legacy Services. Sometimes it becomes technically impossible to continue a feature or we redesign a part of our Services because we think it could be better or we decide to close new signups of a product. We reserve the right at any time to modify or discontinue, temporarily or permanently, any part of our Services with or without notice.
2. Sometimes we change the pricing structure for our products. When we do that, we tend to exempt existing customers from those changes. However, we may choose to change the prices for existing customers. If we do so, we will give at least 30 days notice and will notify you via the email address on record. We may also post a notice about changes on our websites or the affected Services themselves.
## Uptime, Security, and Privacy
1. Your use of the Services is at your sole risk. We provide these Services on an “as is” and “as available” basis. We do not offer service-level agreements for most of our Services, but do take uptime of our applications seriously.
2. We reserve the right to temporarily disable your account if your usage significantly exceeds the average usage of other customers of the Services. Of course, we’ll reach out to the account owner before taking any action except in rare cases where the level of use may negatively impact the performance of the Service for other customers.
3. We take many measures to protect and secure your data through backups, redundancies, and encryption. We enforce encryption for data transmission from the public Internet. If you find a security flaw in our systems, please report it as soon as possible to [report@pijul.org](mailto:report@pijul.org). As we strive to be as open and transparent as possible, we will publish actual flaws immediately.
4. As a European company, we comply with GDPR, as described in our [Privacy Policy](../privacy). When you use our Services, you entrust us with your data, and agree that Pijul may process your data and for no other purpose. We as humans can access your data for the following reasons:
- **To help you with support requests you make.** We’ll ask for express consent before accessing your account.
- **On the rare occasions when an error occurs that stops an automated process partway through.** We get automated alerts when such errors occur. When we can fix the issue and restart automated processing without looking at any personal data, we do. In rare cases, we have to look at a minimum amount of personal data to fix the issue. In these rare cases, we aim to fix the root cause to prevent the errors from recurring.
- **To safeguard Pijul OÜ.** We’ll look at logs and metadata as part of our work to ensure the security of your data and the Services as a whole. If necessary, we may also access accounts as part of an [abuse report investigation](../terms/abuse).
- **To the extent required by applicable law.** As an Estonian company, we only preserve or share customer data if compelled by an Estonian government authority with a legally binding order or proper request, or in limited circumstances in the event of an emergency request. If a non-Estonian authority approaches Pijul for assistance, our default stance is to refuse unless the order has been approved by the Estonian government, which compels us to comply through procedures outlined in an established mutual legal assistance treaty or agreement mechanism. If Pijul OÜ is audited by a tax authority, we only share the bare minimum billing information needed to complete the audit.
5. We use third party vendors and hosting partners to provide the necessary hardware, software, networking, storage, and related technology required to run the Services. In particular, our web services run on the Cloudflare Workers platform (see their [Data Processing Addendum](https://www.cloudflare.com/cloudflare-customer-dpa/), and our emails are sent using Amazon SES (see their [Data Processing Addendum](https://d1.awsstatic.com/legal/aws-gdpr/AWS_GDPR_DPA.pdf)).
6. Under the California Consumer Privacy Act (“CCPA”), Pijul OÜ is a “service provider”, not a “business” or “third party”, with respect to your use of the Services. That means we process any data you share with us only for the purpose you signed up for and as described in these Terms, the [Privacy policy](../privacy). We do not retain, use, disclose, or sell any of that information for any other commercial purposes unless we have your explicit permission. And on the flip-side, you agree to comply with your requirements under the CCPA and not use Pijul’s Services in a way that violates the regulations.
<!--7. These Terms incorporate the [37signals Data Processing Addendum (“DPA”)](https://basecamp.com/about/policies/privacy/regulations/basecamp-gdpr-dpa.pdf) when the EU General Data Protection Regulation (“GDPR”) or United Kingdom General Data Protection Regulation (“UK GDPR”) applies to your use of 37signals Services to process Customer Data as defined in the DPA. The DPA linked above supersedes any previously agreed data processing addendum between you and 37signals LLC relating to your use of the 37signals Services.-->
## Copyright and Content Ownership
1. All content posted on the Services must comply with EU copyright law.
2. You give us a limited license to use the content posted by you and your users in order to provide the Services to you, but we claim no ownership rights over those materials. All materials you submit to the Services remain yours.
3. We do not pre-screen content, but we reserve the right (but not the obligation) in our sole discretion to refuse or remove any content that is available via the Service.
4. The Company or its licensors own all right, title, and interest in and to the Services, including all intellectual property rights therein, and you obtain no ownership rights in the Services as a result of your use. You may not duplicate, copy, or reuse any portion of the HTML, CSS, JavaScript, or visual design elements without express written permission from the Company, unless explicitly stated otherwise. You must request permission to use the Company’s logos or any Service logos for promotional purposes. Please [email us](mailto:contact@pijul.org) requests to use logos. We reserve the right to rescind any permissions if you violate these Terms.
5. You agree not to reproduce, duplicate, copy, sell, resell or exploit any portion of the Services, use of the Services, or access to the Services without the express written permission of the Company.
## Features and Bugs
We design our Services with care, based on our own experience and the experiences of customers who share their time and feedback. However, there is no such thing as a service that pleases everybody. We make no guarantees that our Services will meet your specific requirements or expectations.
We also test all of our features extensively before shipping them. As with any software, our Services inevitably have some bugs. We track the bugs reported to us and work through priority ones, especially any related to security or privacy. Not all reported bugs will get fixed and we don’t guarantee completely error-free Services.
## Services Adaptations and API Terms
We offer Application Program Interfaces (“API”s) for some of our Services. Any use of the API, including through a third-party product that accesses the Services, is bound by these Terms plus the following specific terms:
1. You expressly understand and agree that we are not liable for any damages or losses resulting from your use of the API or third-party products that access data via the API.
2. Third parties may not access and employ the API if the functionality is part of an application that remotely records, monitors, or reports a Service user’s activity *other than time tracking*, both inside and outside the applications. The Company, in its sole discretion, will determine if an integration service violates this bylaw. A third party that has built and deployed an integration for the purpose of remote user surveillance will be required to remove that integration.
3. Abuse or excessively frequent requests to the Services via the API may result in the temporary or permanent suspension of your account’s access to the API. The Company, in its sole discretion, will determine abuse or excessive usage of the API. If we need to suspend your account’s access, we will attempt to warn the account owner first. If your API usage could or has caused downtime, we may cut off access without prior notice.
## Liability
We mention liability throughout these Terms but to put it all in one section:
***You expressly understand and agree that the Company shall not be liable, in law or in equity, to you or to any third party for any direct, indirect, incidental, lost profits, special, consequential, punitive or exemplary damages, including, but not limited to, damages for loss of profits, goodwill, use, data or other intangible losses (even if the Company has been advised of the possibility of such damages), resulting from: (i) the use or the inability to use the Services; (ii) the cost of procurement of substitute goods and services resulting from any goods, data, information or services purchased or obtained or messages received or transactions entered into through or from the Services; (iii) unauthorised access to or alteration of your transmissions or data; (iv) statements or conduct of any third party on the service; (v) or any other matter relating to these Terms or the Services, whether as a breach of contract, tort (including negligence whether active or passive), or any other theory of liability.***
In other words: choosing to use our Services does mean you are making a bet on us. If the bet does not work out, that’s on you, not us. We do our darnedest to be as safe a bet as possible through careful management of the business; investments in security, infrastructure, and talent. If you choose to use our Services, thank you for betting on us.
If you have a question about any of these Terms, please [contact our Support team](mailto:contact@pijul.org).
Adapted from the [Basecamp open-source policies](https://github.com/basecamp/policies) / [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
<style>
h1 { font-size: 30pt; }
h2 { font-size: 20pt; }
</style>
<div class="p-3">
<h3>Account created</h3>
<div class="p-3">
<p>You should receive a confirmation email shortly.</p>
<p>Click on the link in that email to log in.</p>
</div>
</div>
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { errorMsg, server } from '../../helpers';
export const load: PageLoad = async ({ url, fetch, params }) => {
let url_ = `${server}/api/settings/ssh`;
let resp = await fetch(url_, { credentials: 'include' });
console.log('_status_', resp.status, url);
let y = await resp.json();
console.log('y', y, y.is_owner);
return y;
};
<script lang="ts">
import type { PageData } from './$types';
import { onDestroy } from 'svelte';
export let data: PageData;
console.log('data', data);
let m: any = null;
onDestroy(() => {
if (m) {
m.hide();
}
});
</script>
<h1>SSH Keys</h1>
<div class="py-4">
<ul class="list-group list-group-flush px-0">
{#each Object.entries(data.ssh_keys) as [i, key]}
<li class="list-group-item py-5 px-0">
<form action="/api/ssh/delete" method="post" id="ssh-delete-{i}">
<input type="hidden" name="token" value={data.token} />
</form>
<form class="d-flex flex-column" action="/api/settings/ssh" method="post">
<input type="hidden" name="id" value={i} />
<input type="hidden" name="token" value={data.token} />
<textarea class="textarea font-mono key w-full" name="key">{key}</textarea>
<div class="mt-2 ms-auto">
<button class="btn btn-sm btn-error" form="ssh-delete-{i}" name="delete" value={i}>
<span class="icon-[tdesign--delete-1] w-4 h-4"></span> Delete
</button>
<button class="btn btn-sm btn-primary">Update</button>
</div>
</form>
</li>
{/each}
<li class="list-group-item py-5 px-0">
<form class="d-flex flex-column" action="/api/settings/ssh/add" method="post">
<input type="hidden" name="token" value={data.token} />
<textarea class="textarea font-mono key w-full" name="key" placeholder="new SSH public key"
></textarea>
<div class="mt-2 ms-auto">
<button class="btn btn-sm btn-primary">Add</button>
</div>
</form>
</li>
</ul>
</div>
<style>
.key {
font-size: 80%;
min-height: 120px;
}
</style>
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>Account</h1>
<div class="py-4">
Enter your password here to delete your account:
<form method="POST" action="/api/settings/delete">
<div class="flex align-center gap-4 py-5">
<input type="hidden" name="token" value={data.token} />
<label class="label" for="password">Password</label>
<input type="password" class="input" id="password" name="password" placeholder="Password" />
<button class="btn btn-error">Delete my account</button>
</div>
</form>
</div>
<script lang="ts">
export let login: string;
export let page: string;
</script>
<div class="col-12 col-md-4">
<div class="row d-none d-md-block">
<div class="col text-center">
<img alt={login} class="profile-picture border mb-5" src="/identicon/{login}" />
</div>
</div>
<div class="row">
<div class="col">
<div class="list-group" id="list-tab" role="tablist">
<a
class="list-group-item list-group-item-action{page == '/settings' ? ' active' : ''}"
id="list-repos-list"
href="/settings"
role="tab"
aria-controls="repositories">Repositories</a>
<a
class="list-group-item list-group-item-action{page == '/ssh' ? ' active' : ''}"
id="list-ssh-list"
href="/settings/ssh"
role="tab"
aria-controls="SSH">SSH</a>
<a
class="list-group-item list-group-item-action{page == '/settings/account'
? ' active'
: ''}"
id="list-account"
href="/settings/account"
role="tab"
aria-controls="account">Account</a>
</div>
</div>
</div>
</div>
<style>
img.profile-picture {
object-fit: cover;
width: 100%;
max-width: 150px;
border-radius: 50%;
border: 1px solid #bbb;
margin: 0 auto;
background-color: #fff;
}
</style>
import type { PageLoad } from './$types';
import { server } from '../helpers';
export const load: PageLoad = async ({ url, fetch }) => {
let url_ = `${server}/api/settings`;
let resp = await fetch(url_, { credentials: 'include' });
console.log('_status_', resp.status, url);
let y = await resp.json();
console.log('y', y, y.is_owner);
return y;
};
<script lang="ts">
import type { PageData } from './$types';
import { enhance } from '$app/forms';
export let data: PageData;
console.log('data', data);
async function delRepo(ev: { cancel: () => void }, name: string) {
if (!window.confirm(`Kill ${name}?`)) {
ev.cancel();
}
}
</script>
<h1>Repositories</h1>
<div class="py-5">
{#if Object.keys(data.repos).length}
<ul class="">
{#each Object.keys(data.repos) as repo}
<li class="">
<form
method="POST"
action="/api/settings/repo/delete"
use:enhance={(ev) => delRepo(ev, repo)}>
<input type="hidden" name="token" value={data.token} />
<a id="repo-{repo}" href="/{data.login}/{repo}">{repo}</a>
{#if data.repos[repo].private}
<i class="bi bi-lock-fill" style="color:#aaa"></i>
{/if}
<button class="mx-3" name="repo" value={repo} aria-label="Delete">
<span class="icon-[tdesign--delete-1] w-4 h-4 text-error"></span>
</button>
</form>
</li>
{/each}
</ul>
{:else}
<p>You haven't created any repository yet</p>
{/if}
</div>
<h2>Create a repository</h2>
<div class="py-4">
<form method="post" action="/api/settings/repo/add">
<input type="hidden" name="token" value={data.token} />
<ul class="list-group list-group-flush">
<li class="flex content-between items-center gap-5">
<input
type="text"
name="name"
placeholder="Repository name"
class="input input-sm col"
autocomplete="off" />
<div>
<input type="checkbox" class="checkbox checkbox-sm" id="private" name="private" /><label
class="ms-2 label"
for="private">Private</label>
</div>
<button id="create_repository" class="btn btn-sm btn-primary">Create</button>
</li>
</ul>
</form>
</div>
import type { LayoutLoad } from './$types';
import { server } from '../helpers';
export const load: LayoutLoad = async ({ url, fetch }) => {
let url_ = `${server}/api/settings`;
let resp = await fetch(url_, { credentials: 'include' });
console.log('_status_', resp.status, url);
let y = await resp.json();
console.log('y', y, y.is_owner);
return y;
};
<script lang="ts">
import { page } from '$app/state';
import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props();
</script>
<div class="p-3 flex flex-col gap-5 items-center">
<img alt={data.login} class="my-5 profile-picture border" src="/identicon/{data.login}" />
<div class="tabs tabs-border" id="list-tab" role="tablist">
<a
class="tab{page.url.pathname == '/settings' ? ' tab-active' : ''}"
id="list-repos-list"
href="/settings"
role="tab"
aria-controls="repositories">Repositories</a>
<a
class="tab{page.url.pathname == '/settings/ssh' ? ' tab-active' : ''}"
id="list-ssh-list"
href="/settings/ssh"
role="tab"
aria-controls="SSH">SSH</a>
<a
class="tab{page.url.pathname == '/settings/account' ? ' tab-active' : ''}"
id="list-account"
href="/settings/account"
role="tab"
aria-controls="account">Account</a>
</div>
</div>
<div class="col p-3">
<div role="tabpanel" id="nav-tabContent">
<div class="active" role="tabpanel" aria-labelledby="list-repos-list">
{@render children()}
</div>
</div>
</div>
<style>
:global {
img.profile-picture {
object-fit: cover;
width: 100%;
max-width: 150px;
border-radius: 50%;
border: 1px solid #bbb;
background-color: #fff;
}
}
</style>
<div class="p-3">
<h3>Account recovery</h3>
<div class="p-3">
<p>You should receive a confirmation email shortly.</p>
<p>Click on the link in that email to reset your password.</p>
</div>
</div>
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
let code = url.searchParams.get('code');
if (code) {
return { code };
} else {
return null;
}
};
<script lang="ts">
export let data: null | { code: string };
</script>
<svelte:head>
<title>Home</title>
</svelte:head>
<h5 class="my-5 mx-auto">Password recovery</h5>
{#if data?.code}
<form method="POST" action="/recover/reset">
<input type="hidden" name="code" value={data.code} />
<label class="form-label mt-2" for="pass">New password</label>
<input class="form-control" type="password" name="pass" placeholder="Password" />
<label class="form-label mt-2" for="pass2">Confirm</label>
<input class="form-control" type="password" name="pass2" placeholder="Password" />
<div class="mt-4 text-center">
<button class="btn btn-primary">Ok</button>
</div>
</form>
{:else}
<form method="POST" action="/recover/init">
<label class="form-label" for="login">Login or email</label>
<input class="form-control" type="text" name="login" placeholder="Login" />
<div class="mt-4 text-center">
<button class="btn btn-primary">Ok</button>
</div>
</form>
{/if}
---
title: Privacy policy
description: The privacy of your data — and it is your data, not ours! — is a big deal to us. Here’s the rundown of what we collect and why, when we access your information, and your rights.
---
# Privacy policy
*Last updated: May 19th, 2023*
The privacy of your data—and it is your data, not ours!—is a big deal to us. In this policy, we lay out: what data we collect and why; how your data is handled; and your rights with respect to your data. We promise we never sell your data: never have, never will.
This policy is split into sections. For your convenience, links to each of those sections is as follows:
* [Privacy policy](#privacy-policy)
* [What we collect and why](#what-we-collect-and-why)
* [When we access or disclose your information](#when-we-access-or-disclose-your-information)
* [Your rights with respect to your information](#your-rights-with-respect-to-your-information)
* [How we secure your data](#how-we-secure-your-data)
* [What happens when you delete content in your product accounts](#what-happens-when-you-delete-content-in-your-product-accounts)
* [Data retention](#data-retention)
* [Location of site and data](#location-of-site-and-data)
* [When transferring personal data from the EU](#when-transferring-personal-data-from-the-eu)
* [Changes & questions](#changes--questions)
This policy applies to all products built and maintained by Pijul OÜ ("Pijul"), including all versions of The Nest.
This policy applies to our handling of information about site visitors, prospective customers, and customers and authorised users (in relation to their procurement of the services and management of their relationship with Pijul OÜ). We refer collectively to these categories of individuals as "you" throughout this policy.
<!--
However, this policy does not cover information about a customer's end users that 37signals receives from a customer, or otherwise processes on a customer’s behalf, in connection with the services provided by 37signals to the customer pursuant to an applicable services agreement (including the content of messages of customer end users ("End User Communications")). 37signals processes End User Communications under the instructions of the relevant customer, which is the "data controller" or "business" (or occupies a similar role as defined in applicable privacy laws), as described in the applicable services agreement between such customer and 37signals. 37signals’s obligations as a "data processor" or "service provider" with respect to such information are defined in such services agreement and applicable data protection addendum and are not made part of this policy.
If you are a customer’s end user and you have questions about how your information is collected and processed through the services, please contact the organization who has provided your information to us for more information.
-->
If you are a California resident, please [click here to see our California Notice at Collection](regulations/ccpa/index.md), which includes additional disclosures as required by California law.
## What we collect and why
Our guiding principle is to collect only what we need. Here’s what that means in practice:
### Identity & access
When you sign up for a Pijul product, we ask for identifying information such as your email address, and maybe a company name. That’s so you can personalise your new account, and we can send you product updates and other essential information.
<!--
We may also send you optional surveys from time to time to help us understand how you use our products and to make improvements. With your consent, we will send you our newsletter and other updates.
-->
We also give you the option to add a profile picture that displays in the Nest.
We’ll never sell your personal information to third parties, and we won’t use your name or company in marketing statements without your permission either.
### Billing information
If you sign up for a paid Pijul product, you will be asked to provide your payment information and billing address. Credit card information is submitted directly to our payment processor and doesn’t hit Pijul servers. We store a record of the payment transaction, including the last 4 digits of the credit card number, for purposes of account history, invoicing, and billing support. We store your billing address so we can charge you for service, calculate any sales tax due, send you invoices, and detect fraudulent credit card transactions. We occasionally use aggregate billing information to guide our marketing efforts.
### Product interactions
We store on our servers the content that you upload or receive or maintain in your Nest accounts. This is so you can use our products as intended, for example, to create repositories or to contribute to other projects in The Nest. We keep this content as long as your account is active. If you delete your account, we will pseudonymise your comments on discussions within 24 hours and we will delete the other content within 60 days.
### General Geolocation data
For the Nest, we log the full IP address used to sign up a product account and retain that for use in mitigating future spammy signups. We also log all account access by full IP address for security and fraud prevention purposes, and we keep this login data for as long as your product account is active.
<!--
### Website interactions
We collect information about your browsing activity for analytics and statistical purposes such as conversion rate testing and experimenting with new product designs. This includes, for example, your browser and operating system versions, your IP address, which web pages you visited and how long they took to load, and which website referred you to us. If you have an account and are signed in, these web analytics data are tied to your IP address and user account until your account is no longer active. The web analytics we use are described further in the Advertising and Cookies section.
### Anti-bot assessments
We use [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) across our applications to mitigate brute force logins and as a means of spam protection. We have a legitimate interest in protecting our apps and the broader Internet community from credential stuffing attacks and spam. When you log into your 37signals accounts and when you fill in certain forms in HEY, the CAPTCHA service evaluates various information (e.g., IP address, how long the visitor has been on the app, mouse movements) to try to detect if the activity is from an automated program instead of a human. The CAPTCHA service then provides 37signals with the spam score results; we do not have access to the evaluated information.
### Advertising and Cookies
37signals runs contextual ads on various third-party platforms such as Google, Reddit, and LinkedIn. Users who click on one of our ads will be sent to the Basecamp marketing site. Where permissible under law, we may load an ad-company script on their browsers that sets a third-party cookie and sends information to the ad network to enable evaluation of the effectiveness of our ads, e.g., which ad they clicked and which keyword triggered the ad, and whether they performed certain actions such as clicking a button or submitting a form.
We also use persistent first-party cookies and some third-party cookies to store certain preferences, make it easier for you to use our applications, and perform A/B testing as well as support some analytics.
A cookie is a piece of text stored by your browser. It may help remember login information and site preferences. It might also collect information such as your browser type, operating system, web pages visited, duration of visit, content viewed, and other click-stream data. You can adjust cookie retention settings and accept or block individual cookies in your browser settings, although our apps won’t work and other aspects of our service may not function properly if you turn cookies off.
-->
### Cookies
We use persistent first-party cookies for the sake of authentication, and temporary cookies (with time to live of at most two minutes) for the proper functioning of our services.
### Voluntary correspondence
When you email Pijul with a question or to ask for help, we keep that correspondence, including your email address, so that we have a history of past correspondence to reference if you reach out in the future.
<!--
We also store information you may volunteer, for example, written responses to surveys. If you agree to a customer interview, we may ask for your permission to record the conversation for future reference or use. We will only do so with your express consent.
### How we approach mobile app permissions
We offer optional desktop and mobile apps for some of our products. Because of how the platforms are designed, our apps typically must request your consent before accessing contacts, calendar, camera, and other privacy-sensitive features of your device. Consent is always optional and our apps will function without it, though some features may be unavailable. There are a few exceptions, for example:
* Our iOS apps will ask for permission to use push notifications upon first sign-in.
* Android apps do not require permission to send push notifications.
* For our Basecamp 2 app on Android 5, the OS will prompt for permission upon installation to read and write to your device files so that you can select attachments from your device to upload to the app, and can download files from the app to your device.
-->
## When we access or disclose your information
**To help you troubleshoot or squash a software bug, with your permission.** If at any point we need to access your content to help you with a support case, we will ask for your consent before proceeding.
**To investigate, prevent, or take action regarding restricted uses.** Accessing a customer’s account when investigating potential abuse is a measure of last resort. We want to protect the privacy and safety of both our customers and the people reporting issues to us, and we do our best to balance those responsibilities throughout the process. If we discover you are using our products for a restricted purpose, we will take action as necessary, including notifying appropriate authorities where warranted.
**Aggregated and de-identified data.** We may aggregate and/or de-identify information collected through the services. We may use de-identified or aggregated data for any purpose, including marketing or analytics.
**When required under applicable law.** Pijul OÜ is an Estonian company, subject to Estonian and European rules and laws.
* Requests for user data. Our policy is to not respond to government requests for user data unless we are compelled by legal process or in limited circumstances in the event of an emergency request. It is Pijul’s policy to notify affected users before we disclose data unless we are legally prohibited from doing so, and except in some emergency cases.
* If we are audited by a tax authority, we may be required to disclose billing-related information. If that happens, we will disclose only the minimum needed, such as billing addresses and tax exemption information.
## Your rights with respect to your information
At Pijul, we strive to apply the same data rights to all customers, regardless of their location. Some of these rights include:
* **Right to Know.** You have the right to know what personal information is collected, used, shared or sold. We outline both the categories and specific bits of data we collect, as well as how they are used, in this privacy policy.
* **Right of Access.** This includes your right to access the personal information we gather about you, and your right to obtain information about the sharing, storage, security and processing of that information.
* **Right to Correction.** You have the right to request correction of your personal information.
* **Right to Erasure / “To Be Forgotten”.** This is your right to request, subject to certain limitations under applicable law, that your personal information be erased from our possession and, by extension, from all of our service providers. Fulfilment of some data deletion requests may prevent you from using Pijul services because our applications may then no longer work. In such cases, a data deletion request may result in closing your account.
* **Right to Complain.** You have the right to make a complaint regarding our handling of your personal information with the appropriate supervisory authority.
* **Right to Restrict Processing.** This is your right to request restriction of how and why your personal information is used or processed. (Again: we never have and never will sell nor share your personal data.)
* **Right to Object.** You have the right, in certain situations, to object to how or why your personal information is processed.
* **Right to Portability.** You have the right to receive the personal information we have about you and the right to transmit it to another party. If you want to export data from your accounts, you can contact [privacy@pijul.org](mailto:privacy@pijul.org).
* **Right to not Be Subject to Automated Decision-Making.** You have the right to object to and prevent any decision that could have a legal or similarly significant effect on you from being made solely based on automated processes. This right is limited if the decision is necessary for performance of any contract between you and us, is allowed by applicable law, or is based on your explicit consent.
* **Right to Non-Discrimination.** We do not and will not charge you a different amount to use our products, offer you different discounts, or give you a lower level of customer service because you have exercised your data privacy rights. However, the exercise of certain rights may, by virtue of your exercising those rights, prevent you from using our Services.
Many of these rights can be exercised by signing in and updating your account information. Please note that certain information may be exempt from such requests under applicable law. For example, we need to retain certain information in order to provide our services to you.
In some cases, we also need to take reasonable steps to verify your identity before responding to a request, which may include, at a minimum, depending on the sensitivity of the information you are requesting and the type of request you are making, verifying your name and email address. If we are unable to verify you, we may be unable to respond to your requests. If you have questions about exercising these rights or need assistance, please contact us at [privacy@pijul.org](mailto:privacy@pijul.org). If an authorised agent is corresponding on your behalf, we will need written consent with a signature from the account holder before proceeding.
Depending on applicable law, you may have the right to appeal our decision to deny your request, if applicable. We will provide information about how to exercise that right in our response denying the request. You also have the right to lodge a complaint with a supervisory authority. If you are in the EU or UK, you can contact your data protection authority to file a complaint or learn more about local privacy laws.
## How we secure your data
All data is encrypted via [SSL/TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) when transmitted from our servers to your browser. The database backups are also encrypted. In addition, we go to great lengths to secure your data at rest.
Most data are not encrypted while they live in our database (since they need to be ready to send to you when you need them).
## What happens when you delete content in your accounts
In the Nest, we give you the option to trash content. Anything you trash in your product accounts will become unavailable. The trashed content cannot be accessed via the application and we are not able to retrieve it for you.
If you choose to cancel your account, your content will become immediately inaccessible and should be purged from our systems in full within 60 days, with the exception of content contributed to other projects, when necessary for the correct function of these projects. This applies both for cases when an account owner directly cancels and for auto-cancelled accounts.
## Data retention
We keep your information for the time necessary for the purposes for which it is processed. The length of time for which we retain information depends on the purposes for which we collected and use it and your choices, after which time we may delete and/or aggregate it. We may also retain and use this information as necessary to comply with our legal obligations, resolve disputes, and enforce our agreements. Through this policy, we have provided specific retention periods for certain types of information.
## Location of site and data
Our products and other web properties are operated on a globally-replicated infrastructure. If you are located in the European Union, UK, **please be aware that any information you provide to us will be transferred to and stored in multiple places, including outside of the EU or UK**. By using our websites or Services and/or providing us with your personal information, you consent to this transfer.
## When transferring personal data from the EU
The European Data Protection Board (EDPB) has issued guidance that personal data transferred out of the EU must be treated with the same level of protection that is granted under EU privacy law.
UK law provides similar safeguards for UK user data that is transferred out of the UK. Since the data handled by Pijul is stored on Cloudflare's infrastructure, including on a global, distributed database, data produced in the EU or UK may be replicated elsewhere. See [Cloudflare's DPA](https://www.cloudflare.com/cloudflare-customer-dpa/) for more information. Moreover, we use Amazon Web Services to send our emails, see the [AWS DPA](https://d1.awsstatic.com/legal/aws-gdpr/AWS_GDPR_DPA.pdf).
## Changes & questions
We may update this policy as needed to comply with relevant regulations and reflect any new practices. Whenever we make a significant change to our policies, we will refresh the date at the top of this page and take any other appropriate steps to notify users.
Have any questions, comments, or concerns about this privacy policy, your data, or your rights with respect to your information? Please get in touch by emailing us at [privacy@pijul.org](mailto:privacy@pijul.org) and we’ll be happy to try to answer them!
Adapted from the [Basecamp open-source policies](https://github.com/basecamp/policies) / [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
<style>
h1 { font-size: 30pt; }
h2 { font-size: 20pt; }
h3 { font-size: 15pt; }
</style>
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
cookies.delete('session_id', { path: '/' });
throw redirect(302, `/`);
};
import { error } from '@sveltejs/kit';
import ago from 's-ago';
import { browser } from '$app/environment';
export type Comment = {
author: string;
timestamp: number;
id: string;
content: string;
content_html: string;
};
export const server = browser ? import.meta.env.VITE_SERVER : import.meta.env.VITE_LOCAL_SERVER;
export const ui_server = import.meta.env.VITE_UI_SERVER;
export function timefmt(t_: number | null): string {
if (!t_) {
return '';
}
let t = new Date(t_);
if (new Date().getTime() - t.getTime() < 48 * 3600000) {
return ago(t);
} else {
let f = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
});
return 'on ' + f.format(t);
}
}
export function errorMsg(status: number, y: Object) {
if ('notFound' in y) {
throw error(404);
}
if ('suspended' in y && y.suspended) {
throw error(403, 'This account is suspended.');
}
if ('canRead' in y && !y.canRead) {
throw error(403, 'Insufficient permissions.');
}
if ('canWrite' in y && !y.canWrite) {
throw error(403, 'Insufficient permissions.');
}
throw error(status);
}
<script lang="ts">
</script>
<h4>We sent you a verification link</h4>
<div>
Please check your inbox (and maybe you junk folder) and click on the link to confirm your account.
</div>
import type { PageLoad } from './$types';
import { errorMsg, server } from '../../../../helpers';
import hljs from 'highlight.js';
export const load: PageLoad = async ({ fetch, params }) => {
let u = params.pos
? `${server}/api/tree/${params.user}/${params.repo}?pos=${params.pos}`
: `${server}/api/tree/${params.user}/${params.repo}`;
console.log('url', u);
let resp = await fetch(u, { credentials: 'include' });
console.log(resp);
if (resp.status == 200) {
let y = await resp.json();
console.log('y', y);
if (y.inode.File?.file) {
console.log('highlighting');
try {
let sp = y.inode.File.path[y.inode.File.path.length - 1].basename.split('.');
const result = hljs.highlight(y.inode.File.file, { language: sp[sp.length - 1] });
y.hled = result.value;
} catch (e) {
console.error(e);
y.hled = y.inode.File.file;
}
}
return y;
} else {
let y = await resp.json();
errorMsg(resp.status, y);
}
};
<script lang="ts">
import Nav from '../../Nav.svelte';
import Tabs from '../../Tabs.svelte';
import ChannelBar from '../../ChannelBar.svelte';
import type { PageData } from './$types';
export let data: PageData;
const channel = data.channel.length ? data.channel : 'main';
</script>
<svelte:head>
<title>{data.owner} / {data.repo}</title>
</svelte:head>
<div class="p-3">
<Nav
user={data.owner}
repo={data.repo}
path={data.inode.Directory ? data.inode.Directory.path : data.inode.File.path}
link={true} />
<Tabs user={data.owner} repo={data.repo} channel={data.channel} active="tree" />
<div class="pt-3">
<ChannelBar
owner={data.owner}
repo={data.repo}
{channel}
channels={data.channels}
tab="tree"
token={data.token}
can_delete={true} />
</div>
{#if data.inode.Directory}
<ul class="mt-5 list bg-base-100 rounded-box shadow-md">
{#each data.inode.Directory.children as ch}
<li class="list-row">
<div>
<div class="text-truncate">
<a
class={ch.is_dir ? 'dir' : 'file'}
href="/{data.owner}/{data.repo}{channel.length ? ':' + channel : ''}/tree/{ch.pos}"
>{ch.name}</a>
</div>
<div class="text-truncate d-sm-inline-block">
<div class="d-block text-truncate"></div>
</div>
<div class="text-truncate pl-3 d-none d-md-inline-block text-right"></div>
</div>
</li>
{/each}
</ul>
{:else}
<pre><code class="mt-5 hljs"
>{#if data.hled}{@html data.hled}{/if}</code></pre>
{/if}
</div>
<style lang="scss">
@media (prefers-color-scheme: dark) {
a.file,
a.dir {
color: #fff;
}
}
@media (prefers-color-scheme: light) {
a.file,
a.dir {
color: #000;
}
}
a.file::before {
@extend .tdesign--file;
}
a.dir::before {
@extend .tdesign--folder;
}
a.file::before,
a.dir::before {
background-size: contain;
display: inline-block;
content: '';
width: 1.1em;
height: 1.1em;
margin-right: 0.6em;
position: relative;
top: 2px;
}
pre code.hljs {
display: block;
overflow-x: auto;
padding: 5px;
}
code.hljs {
padding: 3px 5px;
}
:global {
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
@media (prefers-color-scheme: dark) {
.hljs {
color: #fff;
background: #1c1b1b;
}
.hljs-subst {
color: #fff;
}
.hljs-comment {
color: #999;
}
.hljs-attr,
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-section,
.hljs-selector-tag {
color: #88aece;
}
.hljs-attribute {
color: #c59bc1;
}
.hljs-name,
.hljs-number,
.hljs-quote,
.hljs-selector-id,
.hljs-template-tag,
.hljs-type {
color: #f08d49;
}
.hljs-selector-class {
color: #88aece;
}
.hljs-link,
.hljs-regexp,
.hljs-selector-attr,
.hljs-string,
.hljs-symbol,
.hljs-template-variable,
.hljs-variable {
color: #b5bd68;
}
.hljs-meta,
.hljs-selector-pseudo {
color: #88aece;
}
.hljs-built_in,
.hljs-literal,
.hljs-title {
color: #f08d49;
}
.hljs-bullet,
.hljs-code {
color: #ccc;
}
.hljs-meta .hljs-string {
color: #b5bd68;
}
.hljs-deletion {
color: #de7176;
}
.hljs-addition {
color: #76c490;
}
}
@media (prefers-color-scheme: light) {
.hljs {
color: #2f3337;
background: #f6f6f6;
}
.hljs-subst {
color: #2f3337;
}
.hljs-comment {
color: #656e77;
}
.hljs-attr,
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-section,
.hljs-selector-tag {
color: #015692;
}
.hljs-attribute {
color: #803378;
}
.hljs-name,
.hljs-number,
.hljs-quote,
.hljs-selector-id,
.hljs-template-tag,
.hljs-type {
color: #b75501;
}
.hljs-selector-class {
color: #015692;
}
.hljs-link,
.hljs-regexp,
.hljs-selector-attr,
.hljs-string,
.hljs-symbol,
.hljs-template-variable,
.hljs-variable {
color: #54790d;
}
.hljs-meta,
.hljs-selector-pseudo {
color: #015692;
}
.hljs-built_in,
.hljs-literal,
.hljs-title {
color: #b75501;
}
.hljs-bullet,
.hljs-code {
color: #535a60;
}
.hljs-meta .hljs-string {
color: #54790d;
}
.hljs-deletion {
color: #c02d2e;
}
.hljs-addition {
color: #2f6f44;
}
}
}
</style>
import type { PageLoad } from './$types';
import { errorMsg, server } from '../../../../helpers';
export const load: PageLoad = async ({ url, params, fetch }) => {
console.log('loading new disc', JSON.stringify(params));
let x = await (await fetch(`${server}/api/discussion/${params.user}/${params.repo}/new`)).text();
console.log(x);
return JSON.parse(x);
};
<script lang="ts">
import Nav from '../../Nav.svelte';
import Title from '../Title.svelte';
import Comment from '../Comment.svelte';
export let data: {
owner: string;
repo: string;
login: string;
unique: number;
token: string;
};
console.log('data', data);
</script>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<form action="/api/discussion/{data.owner}/{data.repo}/new" method="POST">
<input type="hidden" name="token" value={data.token} />
<div class="pt-3">
<Title />
</div>
<Comment
owner={data.owner}
repo={data.repo}
edit={true}
token={data.token}
uniq={data.unique}
login={data.login} />
<div class="p-3 flex justify-center">
<input type="hidden" name="uniq" value={data.unique} />
<button type="submit" id="create_discussion" class="btn btn-primary"
>Create discussion</button>
</div>
</form>
</div>
<style>
:global(.profile-picture-small) {
width: 4em;
}
</style>
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
import { server } from '../../../../helpers';
import { browser } from '$app/environment';
export const load: PageLoad = async ({ url, fetch, params }) => {
let x = await await fetch(
`${server}/api/discussion/${params.user}/${params.repo}/${params.disc}`
);
if (x.status == 200) {
let y = await x.json();
console.log('y', browser, y);
if (browser) {
let c = url.searchParams.get('edit_comment');
if (c) {
y.edit_comment = c;
}
}
return y;
} else {
throw error(404);
}
};
<script lang="ts">
import { timefmt } from '../../../../helpers.js';
import Nav from '../../Nav.svelte';
import Tabs from '../../Tabs.svelte';
import Title from '../Title.svelte';
import CommentC from '../Comment.svelte';
import Tag from '../Tag.svelte';
import Patch from '../Patch.svelte';
import type { Comment } from '../../../../helpers.ts';
type Patch_ = {
id: string;
hash: string;
can_add: boolean;
can_remove: boolean;
timestamp: number;
pushed_by: string;
removed?: number;
authors: {
login?: string;
name?: string;
key: string;
}[];
header: {
message: string;
timestamp: string;
};
};
type Tag_ = {
timestamp: number;
author: string;
added: boolean;
id?: string;
tag?: {
name: string;
fg: string;
color: string;
};
};
type DiscussionItem = {
comment?: Comment;
patch?: Patch_;
tag?: Tag_;
};
type DiscussionT = {
n: number;
id: string;
title: string;
author: string;
closed?: number;
opened: number;
};
export let data: {
owner: string;
repo: string;
login: string;
email: string;
edit_comment?: string;
d: DiscussionT;
channels: string[];
default_channel: string;
comments: DiscussionItem[];
uniq: number;
token: string;
};
</script>
<svelte:head>
<title>{data.owner}/{data.repo} - Discussion #{data.d.n} - {data.d.title}</title>
</svelte:head>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<Tabs user={data.owner} repo={data.repo} active="discussion" />
<div class="pt-3">
<Title n={data.d.n} title={data.d.title} />
<div class="my-3">
{#if data.d.closed}
Closed {timefmt(data.d.closed * 1000)}
{:else}
Opened by <a class="author" href="/{data.d.author}">{data.d.author}</a>
{timefmt(data.d.opened * 1000)}
{/if}
</div>
</div>
{#if data.comments.length}
{#each data.comments as c}
{#if c.comment}
<CommentC
disc={data.d}
owner={data.owner}
login={data.login}
comment={c.comment}
repo={data.repo}
edit={c.comment.id == data.edit_comment}
token={data.token}
uniq={data.uniq} />
{:else if c.tag}
<Tag owner={data.owner} repo={data.repo} tag={c.tag} />
{:else if c.patch}
<Patch
can_add={c.patch.can_add}
can_remove={c.patch.can_remove}
id={c.patch.id}
disc={data.d.n}
removed={c.patch.removed}
hash={c.patch.hash}
authors={c.patch.authors}
timestamp={c.patch.timestamp}
pushed_by={c.patch.pushed_by}
owner={data.owner}
repo={data.repo}
header={c.patch.header}
token={data.token} />
{/if}
{/each}
{/if}
{#if data.login}
<CommentC
owner={data.owner}
repo={data.repo}
login={data.login}
edit={true}
comment={{
author: data.login,
id: '',
content: '',
content_html: '',
timestamp: 0
}}
disc={data.d}
token={data.token}
uniq={data.uniq} />
{/if}
</div>
<script lang="ts">
export let n: number | null = null;
export let edit = false;
export let title = '';
</script>
{#if n && !edit}
<div class="col-auto my-3">
<h3>#{n} {title}</h3>
</div>
{:else if n}
<form method="POST">
<div class="flex">
<div>
<h3>#{n}</h3>
</div>
<div class="grow">
<input class="input" type="text" name="new_discussion_name" value={title} />
</div>
<div class="col col-auto">
<button class="btn btn-primary">Ok</button>
</div>
</div>
</form>
{:else}
<div class="my-3">
<input
class="input w-full"
type="text"
name="new_discussion_name"
value={title}
placeholder="Discussion title" />
</div>
{/if}
<script lang="ts">
import { timefmt } from '../../../helpers.js';
type Tag = {
timestamp: number;
author: string;
added: boolean;
id?: string;
tag?: {
name: string;
fg: string;
color: string;
};
};
export let tag: Tag;
</script>
<div class="py-3 flex items-center" id={tag.id}>
<div class="px-3 w-20">
<a href="/{tag.author}">
<img class="profile-picture-small" src="/identicon/{tag.author}/small" alt={tag.author} />
</a>
</div>
<div class="w-full flex items-center">
<span class="icon-[tdesign--tag] w-4 h-4"></span>
<a class="mx-2 author" href="/{tag.author}">{tag.author}</a>
{#if tag.tag}
{#if tag.added}
added
{:else}
removed
{/if}
tag
<span class="badge tag m-1" style="background-color:{tag.tag.color};color:{tag.tag.fg};"
>{tag.tag.name}</span>
{:else if tag.added}
reopened this discussion
{:else}
closed this discussion
{/if}
{timefmt(tag.timestamp * 1000)}
</div>
</div>
<style>
.profile-picture-small {
width: 4em;
background-color: white;
border-radius: 50%;
}
</style>
<script lang="ts">
import { timefmt } from '../../../helpers.js';
export let hash: string;
export let id: string;
export let disc: number;
export let timestamp: number;
export let pushed_by: string;
export let removed: number | undefined;
export let can_add: boolean = false;
export let can_remove: boolean = false;
export let owner: string;
export let repo: string;
export let token: string;
export let authors: {
login?: string;
name?: string;
key: string;
}[] = [];
export let header:
| {
message: string;
timestamp: string;
}
| undefined;
console.log('HEADER', JSON.stringify(header));
</script>
<div class={'py-3 flex' + (removed ? ' opacity-50' : '')} id="{hash}.{timestamp}">
<div class="px-3 w-20">
<a href="/{pushed_by}">
<img class="profile-picture-small" src="/identicon/{pushed_by}/small" alt={pushed_by} />
</a>
</div>
<div class="w-full flex flex-col">
<div class="flex justify-between items-center mb-2">
<div class="grow-1 align-middle">
<a class="author" href="/{pushed_by}">{pushed_by}</a> added a change {timefmt(
timestamp * 1000
)}
</div>
{#if removed && can_add}
<div class="ms-2">
<form action="/api/discussion/{owner}/{repo}/{disc}/add_change" method="POST">
<input type="hidden" name="discussion_change" value={id} />
<input type="hidden" name="token" value={token} />
<button class="btn btn-sm btn-primary">Add back</button>
</form>
</div>
{/if}
{#if !removed && can_remove}
<div class="ms-2">
<form action="/api/discussion/{owner}/{repo}/{disc}/remove_change" method="POST">
<input type="hidden" name="discussion_change" value={id} />
<input type="hidden" name="token" value={token} />
<button class="btn btn-sm btn-error">Remove from discussion</button>
</form>
</div>
{/if}
</div>
<div>
{#if header}
<a href="/{owner}/{repo}/change/{hash}">
{#if header.message.length}
{header.message}
{:else}
<em>(no change message)</em>
{/if}
</a>{#each authors as auth}
{#if auth.login}
, by <a class="author" href="/{auth.login}">{auth.login}</a>
{:else if auth.key}
, by <span class="key break-all">{auth.key}</span>
{/if}
{/each}{#if header.timestamp}
, created {timefmt(Date.parse(header.timestamp))}
{/if}
{/if}
<div class="p-1 break-all">
<code class="hash">{hash}</code>
</div>
</div>
</div>
</div>
<style>
.profile-picture-small {
width: 4em;
background-color: white;
border-radius: 50%;
}
</style>
<script lang="ts">
import { assets } from '$app/paths';
import { timefmt } from '../../../helpers.js';
import type { Comment } from '../../../helpers.ts';
// export let author = '';
// export let id = '';
export let disc: { closed?: number; n: number } | undefined = undefined;
// export let timestamp: number;
// export let value = '';
// export let value_html = '';
export let owner: string;
export let repo: string;
export let edit: boolean = false;
export let login: string;
export let token: string;
export let uniq: number;
export let comment: Comment = {
author: login,
id: '',
timestamp: 0,
content: '',
content_html: ''
};
</script>
<div class="py-3 flex" id={comment.id}>
<div class="px-3 w-20">
<a href="/{comment.author}">
<img
class="profile-picture-small"
src="/identicon/{comment.author}/small"
alt={comment.author} />
</a>
</div>
<div class="w-full flex">
<div class="border rounded border-neutral-500 w-full">
<div class="flex flex-col px-3 pb-3">
<div class="py-3 flex items-center">
<span class="icon-[tdesign--chat-bubble] w-4 h-4"></span>
<a class="mx-2 author" href="/{comment.author}">{comment.author}</a>
{timefmt(comment.timestamp * 1000)}
{#if comment.id && comment.author == login}
<div class="ms-auto">
<button
class="btn btn-sm btn-primary my-0"
form="edit_comment"
name="edit_comment"
value={comment.id}
on:click={() => (edit = true)}>Edit</button>
<noscript>
<form method="GET" action="#{comment.id}" id="edit_comment"></form>
</noscript>
</div>
{/if}
</div>
{#if edit}
{#if disc}
<form
method="POST"
action="/api/discussion/{owner}/{repo}/{disc.n}"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="uniq" value={uniq} />
<input type="hidden" name="token" value={token} />
{#if comment.id.length}
<input type="hidden" name="edit_comment" value={comment.id} />
{/if}
<textarea
class="textarea w-full"
name="new_discussion_comment"
rows="8"
placeholder=""
value={comment.content}></textarea>
<div class="py-1">
<button class="btn btn-secondary btn-sm my-1">
{#if comment.id.length}Update{:else}Comment{/if}
</button>
{#if !comment.id.length}
{#if disc.closed}
<button class="btn btn-secondary btn-sm m-1" name="action" value="reopen"
>Comment and reopen
</button>
{:else}
<button class="btn btn-secondary btn-sm m-1" name="action" value="close"
>Comment and close</button>
{/if}
{/if}
</div>
</form>
{:else}
<textarea
class="textarea w-full"
name="new_discussion_comment"
rows="8"
placeholder=""
value={comment.content}></textarea>
{/if}
{:else}
{@html comment.content_html}
{/if}
</div>
</div>
</div>
</div>
<style>
.profile-picture-small {
width: 4em;
background-color: white;
border-radius: 50%;
}
</style>
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
import { errorMsg, server } from '../../../helpers';
export const load: PageLoad = async ({ url, fetch, params }) => {
console.log(url);
let u = `${server}/api/discussion/${params.user}/${params.repo}${url.search}`;
console.log('querying', u);
let resp = await fetch(u, { credentials: 'include' });
if (resp.status == 200) {
return await resp.json();
} else {
throw error(resp.status);
}
};
<script lang="ts">
import { timefmt } from '../../../helpers.js';
import Nav from '../Nav.svelte';
import Tabs from '../Tabs.svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
export let data: PageData;
let includeClosed = false;
</script>
<svelte:head>
<title>{data.owner}/{data.repo} - Discussions</title>
</svelte:head>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<Tabs user={data.owner} repo={data.repo} active="discussion" />
<div class="pt-5 flex flex-col">
<div class="flex w-full items-center justify-end gap-3">
<div>
<input
type="checkbox"
class="toggle toggle-sm toggle-primary"
id="includeClosed"
bind:checked={includeClosed}
name="closed"
on:change={() => {
console.log('changed', includeClosed);
if (includeClosed) goto(`?closed=true`);
else goto(`?`);
}} />
<label class="label" for="includeClosed"> Include closed </label>
</div>
{#if data.login}
<div class="flex justify-end">
<a class="btn btn-primary btn-sm" href="/{data.owner}/{data.repo}/discussion/new"
>Start a new discussion</a>
</div>
{/if}
</div>
{#if data.discussions.length}
<ul class="list mt-3">
{#each data.discussions as d}
<li class="list-row">
<div class="items-center justify-between">
<div>
<div class="flex items-center">
<h2>
<a href="/{data.owner}/{data.repo}/discussion/{d.num}">#{d.num} {d.title}</a>
</h2>
</div>
<div class="justify-between items-center">
{#if d.closed}
<div class="col col-auto">
Closed {timefmt(d.closed)}
</div>
{:else}
<div class="col col-auto">
Opened by <a class="mx-1" href="/{d.author}">{d.author}</a>
{timefmt(d.opened)}
</div>
{/if}
</div>
</div>
{#if d.changes}
<div class="rounded shadow-sm me-3 text-success">
{#if d.changes == 1}
1 change
{:else}
{d.changes} changes
{/if}
</div>
{/if}
</div>
</li>
{/each}
</ul>
{/if}
</div>
</div>
export type Atom = {
NewVertex?: NewVertex;
EdgeMap?: EdgeMap;
};
export type NewVertex = {
up_context: Position[];
down_context: Position[];
flag: Flag;
};
export type Flag = { bits: number } | number;
export type Meta = { basename: string; metadata: number };
export function bits(f: Flag): number {
return typeof f == 'number' ? f : f.bits;
}
export type Position = {
change: number;
pos: number;
};
export type Vertex = {
change: number;
start: number;
end: number;
};
export type EdgeMap = {
edges: Edge[];
inode: Position;
};
export type Deps = null | { hashes: string[]; deps: string[]; known?: string[] };
export type Edge = {
previous: Flag;
flag: Flag;
from: Position;
to: Vertex;
introduced_by: number;
};
export type Contents =
| {
Add: string;
}
| {
Del: string;
};
export type Hunk = {
FileAdd?: {
meta: Meta;
context: Position[];
contents: Contents[];
};
FileMove?: {
old: Meta[];
meta: Meta;
up: Position[];
down: Position[];
};
FileDel?: {
deleted_names: Meta[];
del: Atom;
content_edges: Edge[];
content: Contents[];
};
FileUndel?: {
undel_names: Meta[];
undel: Atom;
content_edges: Edge[];
content: Contents[];
};
SolveNameConflict?: {
old: Meta[];
name: Atom;
};
UnsolveNameConflict?: {
old: Meta[];
name: Atom;
};
Edit?: {
path: string;
line: number;
atom: Atom;
contents: Contents[];
};
Replacement?: {
inode: number;
path: string;
line: number;
ins: NewVertex;
del: EdgeMap;
ins_contents: Contents[];
del_contents: Contents[];
};
SolveOrderConflict?: {
path: string;
line: number;
atom: Atom;
contents: Contents[];
};
UnsolveOrderConflict?: {
path: string;
line: number;
atom: Atom;
contents: Contents[];
};
ResurrectZombie?: {
path: string;
line: number;
atom: Atom;
contents: Contents[];
};
AddRoot?: {
name: Atom;
atom: Atom;
};
DelRoot?: {
name: Atom;
atom: Atom;
};
};
<script lang="ts">
import type { Deps, Position, Vertex } from './Types';
export let c: Position | Vertex;
export let deps: Deps;
let d: string | null = null;
if (deps && deps.hashes[c.change]) {
d = deps.hashes[c.change];
}
</script>
{#if 'start' in c}
{#if d}
[<a href={d}>{c.change}</a>.{c.start}:{c.end}]
{:else}
[{c.change}.{c.start}:{c.end}]
{/if}
{:else if d}
[<a href={d}>{c.change}</a>.{c.pos}]
{:else}
[{c.change}.{c.pos}]
{/if}
<script lang="ts">
export let user = '';
export let repo = '';
export let linkNest: boolean;
</script>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/{user}">{user}</a></li>
<li class="breadcrumb-item active">
{#if linkNest}<a href="/{user}/{repo}">{repo}</a>{:else}{repo}{/if}
</li>
</ol>
</nav>
<script lang="ts">
import type { Meta } from './Types';
export let m: Meta;
let dir = (m.metadata & 0x200) != 0 ? 'd' : '-';
let p =
(m.metadata & 0x100 ? 'x' : '-') +
(m.metadata & 0x80 ? 'w' : '-') +
(m.metadata & 0x40 ? 'r' : '-') +
(m.metadata & 0x20 ? 'x' : '-') +
(m.metadata & 0x10 ? 'w' : '-') +
(m.metadata & 0x8 ? 'r' : '-') +
(m.metadata & 0x4 ? 'x' : '-') +
(m.metadata & 0x2 ? 'w' : '-') +
(m.metadata & 0x1 ? 'r' : '-');
</script>
{m.basename} <span class="permissions">({dir}{p})</span>
<script lang="ts">
import type { Flag } from './Types';
import { bits } from './Types';
export let flag: Flag;
let flag_ = bits(flag);
</script>
{#if (flag_ & 145) != 0}
{flag_ & 1 ? 'B' : ''}{flag_ & 16 ? 'F' : ''}{flag_ & 128 ? 'D' : ''}
{:else}
∅
{/if}
<script lang="ts">
import Pos from './Pos.svelte';
import Flag from './Flag.svelte';
import type { Deps, Edge } from './Types';
export let edge: Edge;
export let deps: Deps;
</script>
<Flag flag={edge.previous} />:<Flag flag={edge.flag} /><Pos c={edge.from} {deps} /> → <Pos
c={edge.to}
{deps} />
<script lang="ts">
export let contents: (
| {
Add: string;
}
| {
Del: string;
}
)[];
function trim(t: string): string {
if (t[t.length - 1] == '\n') {
return t.slice(0, t.length - 1);
}
return t;
}
</script>
<div>
{#each contents as c}
{#if 'Add' in c}
{#each trim(c.Add).split('\n') as l}
<div class="change_addition">
{#if l}
<code class="hljs text">{l}</code>
{/if}
</div>
{/each}
{:else}
{#each trim(c.Del).split('\n') as l}
<div class="change_deletion">
{#if l}
<code class="hljs text">{l}</code>
{/if}
</div>
{/each}
{/if}
{/each}
</div>
<script lang="ts">
import Pos from './Pos.svelte';
import Edge from './Edge.svelte';
import type { Atom, Deps } from './Types';
export let deps: Deps;
export let atom: Atom;
</script>
{#if atom.EdgeMap}
{@const em = atom.EdgeMap}
<div class="context">
{#each em.edges as e, n}
{#if n > 0},
{/if}<Edge edge={e} {deps} />
{/each}
</div>
{/if}
{#if atom.NewVertex}
{@const c = atom.NewVertex}
<div class="context">
{#if c.up_context.length}
<div class="change-newvertex-up">
{#each c.up_context as cc}<Pos {deps} c={cc} />{/each}
</div>
{/if}
{#if c.down_context.length}
<div class="change-newvertex-down">
{#each c.down_context as cc}<Pos {deps} c={cc} />{/each}
</div>
{/if}
</div>
{/if}
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
import { server } from '../../../../helpers';
export const load: PageLoad = async ({ fetch, params }) => {
let resp = await fetch(`${server}/api/change/${params.user}/${params.repo}/get/${params.hash}`);
if (resp.status == 200) {
let x = await resp.json();
console.log(JSON.stringify(x));
return x;
} else {
throw error(resp.status);
}
};
<script lang="ts">
import Nav from '../../Nav.svelte';
import Atom from './Atom.svelte';
import Pos from './Pos.svelte';
import Edge from './Edge.svelte';
import Contents from './Contents.svelte';
import Meta from './Meta.svelte';
import type { Deps, Hunk } from './Types';
export let data: {
deps: Deps;
authors: { login?: string; key: string }[];
hash: string;
repo: string;
owner: string;
header: {
message: string;
timestamp: string;
description: string | null;
};
hunks: Hunk[];
};
let date = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(data.header.timestamp));
</script>
<svelte:head>
<title>{data.owner} / {data.repo} - {data.hash}</title>
</svelte:head>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<h1 class="mt-5">{data.header.message}</h1>
<div class="flex items-center py-3 gap-5">
{#if data.authors?.length}
{#if data.authors[0]?.login}
<img
class="profile-picture"
src="/identicon/{data.authors[0].login}"
alt="Profile picture of {data.authors[0].login}" />
{:else}
<img alt="" class="profile-picture" src="/identicon/question/small" />
{/if}
{:else}
<img class="profile-picture" src="/identicon/question/small" alt="unknown author" />
{/if}
<div class="flex flex-col">
<div>
{#each data.authors as a, i}{#if i > 0},
{/if}{#if a.login}<a href="/{a.login}">{a.login}</a>{:else}
<span class="font-mono">{a.key}</span>
{/if}{/each}
</div>
<div>
{date}
</div>
<div>
<code class="hash">{data.hash}</code>
</div>
</div>
</div>
<h2 class="mt-8">Dependencies</h2>
<ul class="list my-3">
{#if data.deps?.hashes?.length}
{#each data.deps.hashes.slice(2) as d, i}
{#if i >= data.deps?.deps.length + (data.deps?.known?.length || 0)}<li class="list-row">
[*] <a class="link" href={d}><code class="hash">{d}</code></a>
</li>
{:else}
<li>[{i + 2}] <a class="link" href={d}><code class="hash">{d}</code></a></li>
{/if}
{/each}
{/if}
</ul>
<h2 class="mt-8">Change contents</h2>
<ul class="py-3">
{#if data.hunks}
{#each data.hunks as l}
<li class="">
{#if l.FileAdd}
<h3>File addition: <Meta m={l.FileAdd.meta} /></h3>
<div class="context">
<div class="change-newvertex-up">
{#each l.FileAdd.context as cc}<Pos deps={data.deps} c={cc} />{/each}
</div>
</div>
<div class="p-3">
<Contents contents={l.FileAdd.contents} />
</div>
{:else if l.FileMove}
<h6>
File move:{#each l.FileMove.old as m}
<Meta {m} />{/each} → <Meta m={l.FileMove.meta} />
</h6>
<div class="px-3">
<div class="context">
<div class="change-newvertex-up">
{#each l.FileMove.up as cc}<Pos deps={data.deps} c={cc} />{/each}
</div>
</div>
<div class="context">
<div class="change-newvertex-down">
{#each l.FileMove.down as cc}<Pos deps={data.deps} c={cc} />{/each}
</div>
</div>
</div>
{:else if l.FileDel}
<h6>
File deletion:{#each l.FileDel.deleted_names as m}
<Meta {m} />{/each}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.FileDel.del} />
{#if l.FileDel.content_edges.length}
<div class="context">
{#each l.FileDel.content_edges as e, n}{#if n > 0},
{/if}<Edge edge={e} deps={data.deps} />{/each}
</div>
{/if}
</div>
<div class="p-3">
<Contents contents={l.FileDel.content} />
</div>
{:else if l.FileUndel}
<h6>
File un-deletion:{#each l.FileUndel.undel_names as m}
<Meta {m} />{/each}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.FileUndel.undel} />
<div class="context">
{#each l.FileUndel.content_edges as e, n}{#if n > 0},
{/if}<Edge edge={e} deps={data.deps} />{/each}
</div>
</div>
<div class="p-3">
<Contents contents={l.FileUndel.content} />
</div>
{:else if l.SolveNameConflict}
<h6>
Solving a name conflict:{#each l.SolveNameConflict.old as m}
<Meta {m} />{/each}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.SolveNameConflict.name} />
</div>
{:else if l.UnsolveNameConflict}
<h6>
Unsolving a name conflict:{#each l.UnsolveNameConflict.old as m}
<Meta {m} />{/each}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.UnsolveNameConflict.name} />
</div>
{:else if l.Edit}
<h6>Edit in {l.Edit.path} at line {l.Edit.line}</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.Edit.atom} />
</div>
<div class="px-3 pb-0">
<Contents contents={l.Edit.contents} />
</div>
{:else if l.Replacement}
<h6>Replacement in {l.Replacement.path} at line {l.Replacement.line}</h6>
<div class="px-3">
<Atom deps={data.deps} atom={{ EdgeMap: l.Replacement.del }} />
<Contents contents={l.Replacement.del_contents} />
</div>
<div class="px-3 py-2 m-0">
<Atom deps={data.deps} atom={{ NewVertex: l.Replacement.ins }} />
<Contents contents={l.Replacement.ins_contents} />
</div>
{:else if l.SolveOrderConflict}
<h6>
Solving an order conflict in {l.SolveOrderConflict.path} at line {l.SolveOrderConflict
.line}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.SolveOrderConflict.atom} />
</div>
<div class="p-3 pb-0">
<Contents contents={l.SolveOrderConflict.contents} />
</div>
{:else if l.UnsolveOrderConflict}
<h6>
Unsolving an order conflict in {l.UnsolveOrderConflict.path} at line {l
.UnsolveOrderConflict.line}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.UnsolveOrderConflict.atom} />
</div>
<div class="p-3 pb-0">
<Contents contents={l.UnsolveOrderConflict.contents} />
</div>
{:else if l.ResurrectZombie}
<h6>
Resurrecting zombies in {l.ResurrectZombie.path} at line {l.ResurrectZombie.line}
</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.ResurrectZombie.atom} />
</div>
<div class="p-3 pb-0">
<Contents contents={l.ResurrectZombie.contents} />
</div>
{:else if l.AddRoot}
<h6>Add root</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.AddRoot.name} />
<Atom deps={data.deps} atom={l.AddRoot.atom} />
</div>
{:else if l.DelRoot}
<h6>Del root</h6>
<div class="px-3">
<Atom deps={data.deps} atom={l.DelRoot.name} />
<Atom deps={data.deps} atom={l.DelRoot.atom} />
</div>
{/if}
</li>
{/each}
{:else}
This change cannot be displayed, possibly because it contains binary files.
{/if}
</ul>
</div>
<style global>
img.profile-picture {
width: 50px;
height: 50px;
border-radius: 50%;
padding: 0;
background-color: white;
}
:global {
.hash {
color: var(--color-neutral-500);
}
.change-context,
.permissions {
font-size: 90%;
}
.change-newvertex-up::before {
content: '↑';
}
.change-newvertex-down::before {
content: '↓';
}
div.change_addition::before {
content: '+';
}
div.change_deletion::before {
content: '-';
}
div.change_addition::before,
div.change_deletion::before {
position: relative;
margin: -5px;
display: inline-block;
padding: 5px 15px 5px 5px;
}
pre.change_deletion,
pre.change_addition {
overflow: inherit;
}
.change_deletion code,
.change_addition code {
padding: 4px 5px !important;
margin: -4px 0 !important;
display: inline-block !important;
}
.change_deletion code {
color: #721c24;
background-color: #f8d7da;
}
.change_addition code {
color: #155724;
background-color: #d4edda;
}
.change-context div {
display: inline-block;
}
.change-context div.context:first-child::before {
content: '';
}
.change-context div.context::before {
content: ', ';
}
}
</style>
import type { PageLoad } from './$types';
import { errorMsg } from '../../../helpers';
import { server } from '../../../helpers';
export const load: PageLoad = async ({ url, fetch, params }) => {
let from_ = parseInt(url.searchParams.get('from') || '0') || 0;
let to = parseInt(url.searchParams.get('to') || '0') || 0;
let resp = await fetch(
from_ && to
? `${server}/api/change/${params.user}/${params.repo}/list?from=${from_}&to=${to}`
: from_
? `${server}/api/change/${params.user}/${params.repo}/list?from=${from_}`
: to
? `${server}/api/change/${params.user}/${params.repo}/list?to=${to}`
: `${server}/api/change/${params.user}/${params.repo}/list`
);
if (resp.status == 200) {
return await resp.json();
} else {
let y = await resp.json();
errorMsg(resp.status, y);
}
};
<script lang="ts">
import { timefmt } from '../../../helpers.js';
import Nav from '../Nav.svelte';
import Tabs from '../Tabs.svelte';
import ChannelBar from '../ChannelBar.svelte';
import type { PageData } from './$types';
export let data: PageData;
const channel = data.channel ? data.channel : 'main';
</script>
<svelte:head>
<title>{data.owner} / {data.repo}</title>
</svelte:head>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<Tabs user={data.owner} repo={data.repo} channel={data.channel} active="changes" />
<div class="pt-3">
<ChannelBar
owner={data.owner}
repo={data.repo}
{channel}
channels={data.channels}
token={data.token}
tab="change"
can_delete={true} />
<div class="list">
{#each data.hashes as hash}
<div class="list-row flex py-8" id={hash.hash}>
{#if !hash.authors?.length}
<div class="ms-16" id={hash.hash}>
<a href="/{data.owner}/{data.repo}/change/{hash.hash}"
><code class="hash">{hash.hash}</code>
</a>
</div>
{:else if hash.author?.login}
<div class="size-12">
<a href="/{hash.author.login}">
<img class="profile-picture" alt="" src="/identicon/{hash.author.login}/small" />
</a>
</div>
<div>
<div>
<a class="link" href="/{data.owner}/{data.repo}/change/{hash.hash}"
>{hash.message}</a>
</div>
<div>
Created by <a class="change-authors link" href="/{hash.author.login}"
>{hash.author.login}</a
> {timefmt(hash.timestamp)}
</div>
<div>
<a href="/{data.owner}/{data.repo}/change/{hash.hash}"
><code class="hash">{hash.hash}</code></a>
</div>
</div>
{:else}
<div class="size-12 my-auto">
<img alt="" class="profile-picture" src="/identicon/question/small" />
</div>
<div>
<a class="link" href="/{data.owner}/{data.repo}/change/{hash.hash}">{hash.message}</a>
<div class="break-all">
Created by {hash.authors[0].name || hash.authors[0].key} {timefmt(
hash.timestamp
)}
</div>
<div class="break-all">
<a href="/{data.owner}/{data.repo}/change/{hash.hash}"
><code class="hash">{hash.hash}</code></a>
</div>
</div>
{/if}
{#if hash.unrecordable}
<div class="w-auto shrink-0">
<form method="POST" action="/api/change/{data.owner}/{data.repo}:{channel}/unrecord">
<input type="hidden" name="hash" value={hash.hash} />
<input type="hidden" name="token" value={data.token} />
<button class="btn btn-outline-danger my-2">Unrecord</button>
</form>
</div>
{/if}
</div>
{/each}
</div>
{#if data.hashes.length}
<div class="text-center">
{#if data.range && data.hashes[0].n < data.range[1]}
<a class="btn btn-outline-secondary mx-2" href="?to={data.hashes[0].n + 1}">Previous</a>
{/if}
{#if data.hashes[data.hashes.length - 1].n > 0}
<a class="btn btn-outline-secondary mx-2" href="?from={data.hashes[0].n + 1}">Next</a>
{/if}
</div>
{/if}
</div>
</div>
<style lang="postcss">
img.profile-picture {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
padding: 0;
background-color: white;
}
.hash {
color: var(--color-neutral-500);
}
</style>
export let colors = [
0xf2f3f4, 0x222222, 0xf3c300, 0x875692, 0xf38400, 0xa1caf1, 0xbe0032, 0xc2b280, 0x848482,
0x008856, 0xe68fac, 0x0067a5, 0xf99379, 0x604e97, 0xf6a600, 0xb3446c, 0xdcd300, 0x882d17,
0x8db600, 0x654522, 0xe25822, 0x2b3d26
];
export function fg(color: number): number {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
let l = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
let r = l((color >> 16) / 255);
let g = l(((color >> 8) & 0xff) / 255);
let b = l((color & 0xff) / 255);
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let black_contrast = (lum + 0.05) / 0.05;
let white_contrast = 1.1 / (lum + 0.05);
if (white_contrast > black_contrast) {
return 0xffffff;
} else {
return 0;
}
}
export function hex(color: number): string {
return '#' + ('000000' + color.toString(16)).slice(-6);
}
<script lang="ts">
export let login: string;
export let owner: string;
export let repo: string;
export let perms: number;
export let everybody: boolean = false;
export let n: number;
export let token: string;
const p = [
'Read',
'Create discussion',
'Edit discussion',
'Tag discussion',
'Apply changes',
'Edit channels',
'Edit tags',
'Edit permissions'
];
</script>
<form method="POST" action="/api/admin/{owner}/{repo}/permission">
<input type="hidden" name="token" value={token} />
<div class="flex flex-col mt-10">
<div>
<input
id="login"
type="text"
class="input input-sm"
name={everybody ? '' : 'login'}
value={login}
disabled={everybody || login == owner} />
</div>
<div class="flex flex-wrap gap-5 mt-3">
{#each p as label, k}
<div>
<input
class="checkbox checkbox-sm"
type="checkbox"
name="p{k}"
disabled={login == owner}
checked={!!(perms & (1 << k))}
id="{n}-{k}" />
<label class="label ms-1" for="{n}-{k}"> {label} </label>
</div>
{/each}
</div>
<div class="mt-5">
<button class="btn btn-sm btn-secondary" disabled={login == owner}>Ok</button>
</div>
</div>
</form>
<script lang="ts">
import { colors, fg, hex } from './colors';
export let owner: string;
export let repo: string;
export let n: null | number = null;
export let tagColor: number = colors[0];
export let name = '';
export let token: string;
export let id: string | null = null;
let nn = n == null ? '' : n;
</script>
<noscript>
<style>
.noscriptshow {
display: block !important;
}
.noscriptnoshow {
display: none !important;
}
</style>
</noscript>
<form method="POST" action="/api/admin/{owner}/{repo}/tag">
<input type="hidden" name="token" value={token} />
{#if id}
<input type="hidden" name="id" value={id} />
{/if}
<div class="flex flex-col py-3">
<div>
<input
class="input input-sm my-1"
type="text"
name="tag_name"
bind:value={name}
placeholder="Tag"
required />
</div>
<div class="collapse">
<input type="checkbox" />
<div class="collapse-title px-0">
<button
class="btn btn-sm scr color noscriptnoshow"
style:background-color={hex(tagColor)}
style:border-color={hex(tagColor)}
style:color={hex(fg(tagColor))}
type="button"
data-bs-toggle="collapse"
data-bs-target="#color{nn}"
aria-expanded="false"
aria-controls="color{nn}"
id="colorButton{nn}"
aria-label="tag color"></button>
</div>
<div class="collapse-content">
{#each colors as color}
<input
type="radio"
name="colorp"
bind:group={tagColor}
value={color}
checked={tagColor == color}
class="r"
style="--color: {hex(color)}" />
{/each}
</div>
</div>
<div class="noscriptshow" id="color{nn}">
<div class="flex gap-2 items-center">
{#if n == null}
<button class="btn btn-sm btn-secondary my-1">Add tag</button>
{:else}
<button class="btn btn-sm btn-secondary my-1" name="action" value="edit-tag{n}"
>Ok</button>
<button class="btn btn-sm btn-error my-1" name="action" value="delete-tag{n}"
>Delete</button>
{/if}
</div>
<div class="collapse-content p-3"></div>
</div>
</div>
</form>
<style>
form {
display: inline;
}
input[type='radio'].r {
appearance: none;
border: none;
display: inline-block;
width: 25px;
height: 25px;
border-radius: 50%;
}
@media (prefers-color-scheme: dark) {
input[type='radio'].r:checked {
border: 1px solid white;
margin: -1px 1px 1px -1px;
}
}
@media (prefers-color-scheme: light) {
input[type='radio'].r:checked {
border: 1px solid black;
margin: -1px 1px 1px -1px;
}
}
input[type='radio'].r::before {
content: '';
display: block;
width: 19px;
height: 19px;
border-radius: 50%;
background-color: var(--color);
position: relative;
left: 2px;
top: 2px;
}
input[type='radio'].r:not(:checked):hover {
transform: scale(1.2);
}
button.color::after {
content: ' ';
padding-left: 10px;
}
</style>
import type { PageLoad } from './$types';
import { server } from '../../../helpers';
export const load: PageLoad = async ({ fetch, params }) => {
let resp = await fetch(`${server}/api/admin/${params.user}/${params.repo}`);
let y = await resp.json();
console.log('y', y);
return y;
};
<script lang="ts">
import Nav from '../Nav.svelte';
import Tabs from '../Tabs.svelte';
import { colors } from './colors';
import ColorPicker from './ColorPicker.svelte';
import Permission from './Permission.svelte';
import type { PageData } from './$types';
export let data: PageData;
let newTagName = '';
let newTagColor = colors[0];
</script>
<div class="p-3">
<Nav user={data.owner} repo={data.repo} link={true} />
<Tabs user={data.owner} repo={data.repo} active="admin" />
<div class="pt-5">
<h2>Permissions</h2>
<Permission
login="Everybody"
owner={data.owner}
repo={data.repo}
perms={data.is_private ? 0 : 3}
everybody={true}
token={data.token}
n={0} />
{#each data.permissions as p, n}
<Permission
login={p.login}
owner={data.owner}
repo={data.repo}
perms={p.perm}
token={data.token}
n={n + 2} />
{/each}
<Permission owner={data.owner} repo={data.repo} login="" perms={3} token={data.token} n={1} />
</div>
<div class="pt-10">
<h2>Discussion tags</h2>
{#if data.tags?.length}
{#each data.tags as tag, n}
<ColorPicker
owner={data.owner}
repo={data.repo}
id={tag.id}
bind:name={tag.name}
bind:tagColor={tag.color}
token={data.token}
{n} />
{/each}
{/if}
<ColorPicker
owner={data.owner}
repo={data.repo}
bind:name={newTagName}
bind:tagColor={newTagColor}
token={data.token} />
</div>
</div>
import { redirect } from '@sveltejs/kit';
import type { Cookies } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
savePerm: async (x) => await savePerm(x, false),
newPerm: async (x) => await savePerm(x, true)
};
type ActArg = {
url: URL;
platform: Readonly<App.Platform> | undefined;
request: Request;
cookies: Cookies;
params: { user: string; repo: string };
};
async function savePerm({ url, platform, request, cookies, params }: ActArg, isNew: boolean) {
const data = await request.formData();
console.log('data', JSON.stringify(data));
const login = data.get('login');
const read = data.get('read') == 'on';
const write = data.get('write') == 'on';
let perm = {
login,
read,
write
};
let resp = await platform?.env.repoadmin.fetch(
`https://${url.hostname}/api/a/${params.user}/${params.repo}`,
{
method: 'POST',
body: JSON.stringify({ SavePerm: perm, isNew }),
headers: new Headers([
['Content-Type', 'application/json'],
['Cookie', request.headers.get('cookie') || '']
])
}
);
if (resp.ok) {
let x: null | { key: string; value: string } = await resp.json();
if (x) {
let exp = new Date();
exp.setTime(exp.getTime() + 60000);
cookies.set('perm' + x.key, x.value, { expires: new Date(exp) });
}
}
if (!data.get('noredirect')) {
throw redirect(302, `/${params.user}/${params.repo}/admin`);
} else {
return await resp.json();
}
}
<script lang="ts">
import { page } from '$app/state';
export let user: string;
export let repo: string;
export let login: string | undefined = page.data.login;
export let channel: string | undefined = undefined;
export let active: 'tree' | 'changes' | 'tags' | 'discussion' | 'ci' | 'admin';
</script>
<div role="tablist" class="tabs tabs-border border-neutral-500 border-b-1 pb-4 mt-5">
<li class="tab{active == 'tree' ? ' tab-active' : ''}">
<a href="/{user}/{repo}{channel ? ':' + channel : ''}/tree"
><i class="bi bi-code-slash"></i> Code</a>
</li>
<li class="tab{active == 'changes' ? ' tab-active' : ''}">
<a href="/{user}/{repo}{channel ? ':' + channel : ''}/change"
><i class="bi bi-puzzle"></i> Changes</a>
</li>
<li class="tab{active == 'discussion' ? ' tab-active' : ''}">
<a href="/{user}/{repo}/discussion"><i class="bi bi-chat"></i> Discussions</a>
</li>
{#if user == login}
<li class="tab{active == 'admin' ? ' active' : ''}">
<a class="nav-link" href="/{user}/{repo}/admin"><i class="bi bi-tools"></i> Admin</a>
</li>
{/if}
</div>
<script lang="ts">
export let user = '';
export let repo = '';
export let path: { basename: string; pos: string }[] = [];
export let link = false;
</script>
<nav class="breadcrumbs" aria-label="breadcrumbs">
<ol>
<li><a class="link" href="/{user}">{user}</a></li>
<li>
{#if link}<a class="link" href="/{user}/{repo}">{repo}</a>{:else}{repo}{/if}
</li>
{#each path as p, i}
<li class="{i == path.length - 1 ? 'active' : ''}">
{#if link}<a class="link" href="/{user}/{repo}/tree/{p.pos}">{p.basename}</a>{:else}{p.basename}{/if}
</li>
{/each}
</ol>
</nav>
<script lang="ts">
export let channel: string;
export let channels: string[];
</script>
<select id="channel" class="form-select form-select-sm" name="channel" bind:value={channel}>
{#if channels.length}
{#each channels as c}
<option value={c}>{c}</option>
{/each}
{:else}
<option value="main">main</option>
{/if}
</select>
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
export let owner: string;
export let repo: string;
export let channel: string;
export let channels: string[];
export let can_delete: boolean = false;
export let token: string;
export let tab: string;
function change(e: Event) {
e.preventDefault();
let ch = e.target as HTMLFormElement;
console.log('value', ch.value);
if (ch.value == 'main') {
goto(`/${owner}/${repo}/${tab}`);
} else {
goto(`/${owner}/${repo}:${ch.value}/${tab}`);
}
}
</script>
<div class="flex" role="toolbar">
<div class="input-group my-2">
<form method="GET" action="/{owner}/{repo}">
{#if channels.length}
<select
id="channel"
class="select select-sm w-auto"
name="channel"
on:change={change}
bind:value={channel}>
{#each channels as c}
{#if c == channel}
<option value={c} selected={true}>{c}</option>
{:else}
<option value={c}>{c}</option>
{/if}
{/each}
</select>
{/if}
<noscript>
<button class="btn btn-outline-secondary btn-sm">Change</button>
</noscript>
</form>
</div>
<form method="POST" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="redirect" value={page.url.pathname} />
<input type="hidden" name="token" value={token} />
<div class="input-group px-2 my-2 tooltip">
<input
class="input input-sm w-auto"
type="text"
name="name"
placeholder="Channel name"
autocomplete="off"
required />
</div>
<div class="join me-2 my-2" role="group" aria-label="First group">
{#if can_delete}
<button
class="btn btn-error btn-sm"
formaction="/api/channel/{owner}/{repo}:{channel}/prune"
title="Delete channel">
Delete
</button>
{/if}
<button class="btn btn-sm" formaction="/api/channel/{owner}/{repo}:{channel}/fork">
Fork
</button>
<button
class="btn btn-sm"
formaction="/api/channel/{owner}/{repo}:{channel}/rename"
title="Rename channel">
Rename
</button>
</div>
</form>
</div>
<style>
form {
display: inherit;
}
</style>
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { ui_server } from '../../helpers';
export const load: PageLoad = async ({ params }) => {
throw redirect(302, `${ui_server}/${params.user}/${params.repo}/tree`);
};
<script lang="ts">
import { page } from '$app/stores';
</script>
<h4>Error {$page.status}</h4>
{#if $page.error?.message}
<div>{$page.error?.message}</div>
{/if}
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { errorMsg, server, ui_server } from '../helpers';
export const load: PageLoad = async ({ url, fetch, params }) => {
let url_ = `${server}/api/user/${params.user}`;
let resp = await fetch(url_, { credentials: 'include' });
console.log('status', resp.status, url);
let y = await resp.json();
console.log('y', y, y.is_owner);
if (y.is_owner) {
console.log('redirect', `${ui_server}/settings`);
throw redirect(302, `${ui_server}/settings`);
} else if (y.user) {
return y;
} else {
errorMsg(resp.status, y);
}
};
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
<title>{data.user}</title>
</svelte:head>
<div class="flex gap-5">
<div class="flex flex-col m-3 items-center">
<div class="my-5">
<img
class="profile-picture"
src="/identicon/{data.user}"
alt="Profile picture of {data.user}" />
</div>
<h3>{data.user}</h3>
</div>
<div class="flex flex-col p-5">
<h1>Repositories</h1>
{#if Object.keys(data.repos).length}
<ul class="mt-5">
{#each Object.keys(data.repos) as repo}
<li class="mt-2">
<a class="link" href="/{data.user}/{repo}">
{repo}
</a>
<span class="ms-2 icon-[tdesign--git-repository-private]"></span>
{#if data.repos[repo].private}
<span class="ms-2 icon-[tdesign--git-repository-private]"></span>
{/if}
</li>
{/each}
</ul>
{:else}
{data.user} has not created any public repository yet
{/if}
</div>
</div>
<style>
img.profile-picture {
background-color: white;
object-fit: cover;
width: 100%;
max-width: 150px;
border-radius: 50%;
border: 1px solid #bbb;
margin: 0 auto;
padding: 0;
}
</style>
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { server, ui_server } from './helpers';
export const load: PageLoad = async ({ fetch }) => {
let url_ = `${server}/api/user`;
let resp = await fetch(url_, { credentials: 'include' });
let y = await resp.json();
if (y?.login) {
throw redirect(302, `${ui_server}/${y.login}`);
}
return y;
};
<script lang="ts">
import { page } from '$app/state';
</script>
<svelte:head>
<title>Home</title>
</svelte:head>
<h1 class="my-5 mx-auto">Welcome to the Nest</h1>
<div class="mx-auto mb-5">
The best place to host your <a href="https://pijul.org">Pijul</a> repositories and collaborate with
others.
</div>
<div class="card">
<div class="card-body mx-auto w-96">
<form method="POST" action="/login">
<input class="input" type="text" name="login" placeholder="Login" />
<div class="mt-4">
<input class="input" type="password" name="pass" placeholder="Password" />
</div>
<div class="mt-4">
<a class="link" href="/recover">Click here to reset your password</a>
</div>
<div class="mt-4 text-center">
<button class="btn btn-primary">Ok</button>
</div>
</form>
</div>
</div>
<div class="card mx-auto card-border w-96">
<div class="mx-auto mt-4">Create an account:</div>
<div class="card-body">
<form method="POST" action="/register">
<div class="mt-4">
<input class="input" type="text" name="login" placeholder="Login" autocomplete="off" />
</div>
<div class="mt-4">
<input
class="input"
type="email"
name="email"
placeholder="Email address"
autocomplete="off" />
</div>
<div class="mt-4">
<input
class="input"
type="password"
name="pass"
placeholder="Password"
autocomplete="new-password" />
</div>
<div class="mt-4">
<input
class="input"
type="password"
name="confpass"
placeholder="Confirm password"
autocomplete="new-password" />
</div>
{#if page.url.searchParams.get('error') == 'alreadyExists'}
<div class="mt-4 text-danger">This login is already taken.</div>
{/if}
<div class="mt-4 text-center">
<button class="btn btn-primary">Register</button>
</div>
</form>
</div>
</div>
<script>
import { page } from '$app/state';
import '../app.css';
</script>
<div class="flex flex-col min-h-screen">
<div class="navbar neg">
<div class="container mx-auto flex">
<div class="my-2">
<a href="/">The Nest (beta)</a>
</div>
<div class="my-2 ms-auto">
{#if page.data.login}
Logged in as <a href="/settings" aria-label="Settings">
{page.data.login}
</a>
<a href="/logout" aria-label="Log out">
<span class="ms-2 icon-[tdesign--logout]"></span>
</a>
{/if}
</div>
</div>
</div>
<main class="flex-1 px-3 container mx-auto">
<slot />
</main>
<footer class="neg flex items-center justify-between px-4 min-h-16">
<div class="container mx-auto">
© 2022 by the Pijul team
<a href="/privacy">Privacy policy</a>
<a href="/terms">Terms</a>
</div>
</footer>
</div>
<style>
.neg {
color: #fff;
background-color: #000;
}
.neg a {
color: #fff;
font-weight: bold;
text-decoration: none;
}
</style>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
%sveltekit.head%
</head>
<body data-sveltekit-prefetch class="min-h-screen">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
/// <reference types="@sveltejs/kit" />
/// <reference types="@sveltejs/adapter-cloudflare-workers" />
declare namespace App {
interface Platform {
env: {
user: ServiceWorkerGlobalScope;
tree: ServiceWorkerGlobalScope;
repo: ServiceWorkerGlobalScope;
repoadmin: ServiceWorkerGlobalScope;
change: ServiceWorkerGlobalScope;
settings: ServiceWorkerGlobalScope;
discussions: ServiceWorkerGlobalScope;
account: ServiceWorkerGlobalScope;
stripe: ServiceWorkerGlobalScope;
users: KVNamespace;
cookie_hmac: string;
};
}
}
@import 'tailwindcss';
@plugin "daisyui";
@plugin "@iconify/tailwind4";
@layer base {
h1 { font-size: var(--text-2xl) !important; }
h2 { font-size: var(--text-xl) !important; }
h3 { font-size: var(--text-l) !important; }
}
module.exports = {
plugins: [require('@tailwindcss/postcss'), require('autoprefixer')]
};
{
"name": "ui",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@iconify/json": "^2.2.309",
"@iconify/svelte": "^4.2.0",
"@iconify/tailwind4": "^1.0.3",
"@sveltejs/kit": "^2.17.2",
"@tailwindcss/postcss": "^4.0.8",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"autoprefixer": "^10.4.20",
"daisyui": "^5.0.0-beta.8",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-css": "^0.11.0",
"eslint-plugin-svelte": "^2.46.1",
"mdsvex": "^0.12.3",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.17.3",
"svelte-check": "^4.1.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",
"@tailwindcss/vite": "^4.0.8",
"highlight.js": "^11.11.1",
"s-ago": "^2.2.0",
"tailwindcss": "^4.0.8"
}
}
{
"name": "ui",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "0.0.1",
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",
"@tailwindcss/vite": "^4.0.8",
"highlight.js": "^11.11.1",
"s-ago": "^2.2.0",
"tailwindcss": "^4.0.8"
},
"devDependencies": {
"@iconify/json": "^2.2.309",
"@iconify/svelte": "^4.2.0",
"@iconify/tailwind4": "^1.0.3",
"@sveltejs/kit": "^2.17.2",
"@tailwindcss/postcss": "^4.0.8",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"autoprefixer": "^10.4.20",
"daisyui": "^5.0.0-beta.8",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-css": "^0.11.0",
"eslint-plugin-svelte": "^2.46.1",
"mdsvex": "^0.12.3",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.17.3",
"svelte-check": "^4.1.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@antfu/install-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
"integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"package-manager-detector": "^0.2.8",
"tinyexec": "^0.3.2"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@antfu/utils": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
"integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/config-array": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz",
"integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/js": {
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz",
"integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.12.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.3.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@iconify/json": {
"version": "2.2.309",
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.309.tgz",
"integrity": "sha512-xgaPDmVNeHmggEDHrbyntqPw8YzqIf96XOOqC7HbbDS4lf4a6mvYVCDYINSZjsw2Wvak3CL7PZLQ9ZgqoypMtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iconify/types": "*",
"pathe": "^1.1.2"
}
},
"node_modules/@iconify/svelte": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-4.2.0.tgz",
"integrity": "sha512-fEl0T7SAPonK7xk6xUlRPDmFDZVDe2Z7ZstlqeDS/sS8ve2uyU+Qa8rTWbIqzZJlRvONkK5kVXiUf9nIc+6OOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"svelte": ">4.0.0"
}
},
"node_modules/@iconify/tailwind4": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@iconify/tailwind4/-/tailwind4-1.0.3.tgz",
"integrity": "sha512-qhgEBFwbhoWWGo3X27KInvNQOjt7UwmZTuLpjYQlYMTkcXzwPnomqmxzsBUC2L0ySA105BFDppYBv4CuWBQKwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.2.1"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"tailwindcss": ">= 4"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"dev": true,
"license": "MIT"
},
"node_modules/@iconify/utils": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
"integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@antfu/install-pkg": "^1.0.0",
"@antfu/utils": "^8.1.0",
"@iconify/types": "^2.0.0",
"debug": "^4.4.0",
"globals": "^15.14.0",
"kolorist": "^1.8.0",
"local-pkg": "^1.0.0",
"mlly": "^1.7.4"
}
},
"node_modules/@iconify/utils/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.0",
"@parcel/watcher-darwin-arm64": "2.5.0",
"@parcel/watcher-darwin-x64": "2.5.0",
"@parcel/watcher-freebsd-x64": "2.5.0",
"@parcel/watcher-linux-arm-glibc": "2.5.0",
"@parcel/watcher-linux-arm-musl": "2.5.0",
"@parcel/watcher-linux-arm64-glibc": "2.5.0",
"@parcel/watcher-linux-arm64-musl": "2.5.0",
"@parcel/watcher-linux-x64-glibc": "2.5.0",
"@parcel/watcher-linux-x64-musl": "2.5.0",
"@parcel/watcher-win32-arm64": "2.5.0",
"@parcel/watcher-win32-ia32": "2.5.0",
"@parcel/watcher-win32-x64": "2.5.0"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz",
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz",
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz",
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz",
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz",
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz",
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz",
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz",
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz",
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz",
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz",
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz",
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz",
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz",
"integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz",
"integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
"integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
"integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
"integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
"integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
"integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
"integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
"integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
"integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
"integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
"integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
"integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
"integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
"integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
"integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
"integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
"integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
"integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
"integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
"integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/kit": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.0.tgz",
"integrity": "sha512-UTx28Ad4sYsLU//gqkEo5aFOPFBRT2uXCmXTsURqhurDCvzkVwXruJgBcHDaMiK6RKKpYRteDUaXYqZyGPgCXQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
"devalue": "^5.1.0",
"esm-env": "^1.2.2",
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^3.0.0"
},
"bin": {
"svelte-kit": "svelte-kit.js"
},
"engines": {
"node": ">=18.13"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3 || ^6.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz",
"integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
"debug": "^4.4.0",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.15",
"vitefu": "^1.0.4"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite": "^6.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.3.7"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"vite": "^6.0.0"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz",
"integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==",
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"tailwindcss": "4.0.8"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz",
"integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.0.8",
"@tailwindcss/oxide-darwin-arm64": "4.0.8",
"@tailwindcss/oxide-darwin-x64": "4.0.8",
"@tailwindcss/oxide-freebsd-x64": "4.0.8",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8",
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.8",
"@tailwindcss/oxide-linux-arm64-musl": "4.0.8",
"@tailwindcss/oxide-linux-x64-gnu": "4.0.8",
"@tailwindcss/oxide-linux-x64-musl": "4.0.8",
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.8",
"@tailwindcss/oxide-win32-x64-msvc": "4.0.8"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz",
"integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz",
"integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz",
"integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz",
"integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz",
"integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz",
"integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz",
"integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz",
"integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz",
"integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz",
"integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz",
"integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.8.tgz",
"integrity": "sha512-SUwlrXjn1ycmUbA0o0n3Y0LqlXqxN5R8HR+ti+OBbRS79wl2seDmiypEs3xJCuQXe07ol81s1AmRMitBmPveJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.0.8",
"@tailwindcss/oxide": "4.0.8",
"lightningcss": "^1.29.1",
"postcss": "^8.4.41",
"tailwindcss": "4.0.8"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz",
"integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.0.8",
"@tailwindcss/oxide": "4.0.8",
"lightningcss": "^1.29.1",
"tailwindcss": "4.0.8"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
"integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.19.1",
"@typescript-eslint/type-utils": "8.19.1",
"@typescript-eslint/utils": "8.19.1",
"@typescript-eslint/visitor-keys": "8.19.1",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz",
"integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.19.1",
"@typescript-eslint/types": "8.19.1",
"@typescript-eslint/typescript-estree": "8.19.1",
"@typescript-eslint/visitor-keys": "8.19.1",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz",
"integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.19.1",
"@typescript-eslint/visitor-keys": "8.19.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz",
"integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.19.1",
"@typescript-eslint/utils": "8.19.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz",
"integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz",
"integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.19.1",
"@typescript-eslint/visitor-keys": "8.19.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz",
"integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.19.1",
"@typescript-eslint/types": "8.19.1",
"@typescript-eslint/typescript-estree": "8.19.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.19.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz",
"integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.19.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"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"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/daisyui": {
"version": "5.0.0-beta.8",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.0-beta.8.tgz",
"integrity": "sha512-jSokqm5i+Pv1jG80wliNzMHjmcF+iMx5xRUpk0/QExVoVNyQNWeCsaWJQubPvUq7bt9nzSsQTR2uIZBoyIIoaA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/devalue": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.102",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz",
"integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm": "0.25.0",
"@esbuild/android-arm64": "0.25.0",
"@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-arm64": "0.25.0",
"@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-s390x": "0.25.0",
"@esbuild/linux-x64": "0.25.0",
"@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/netbsd-x64": "0.25.0",
"@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/openbsd-x64": "0.25.0",
"@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz",
"integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.2",
"@eslint/core": "^0.12.0",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "9.21.0",
"@eslint/plugin-kit": "^0.2.7",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.2.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
}
},
"node_modules/eslint-compat-utils": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
"integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-config-prettier": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-css": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-css/-/eslint-plugin-css-0.11.0.tgz",
"integrity": "sha512-83T6HUtCa5et2VLCZlpLhDdyegGRJ6UuibVVaexK/hJcaslfszzFhKbNEckqwens0iLvCDANW0cX0TZgl/9+YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.3.0",
"colord": "^2.9.1",
"eslint-compat-utils": "^0.5.0",
"known-css-properties": "^0.34.0",
"postcss-value-parser": "^4.1.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-css/node_modules/known-css-properties": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz",
"integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-plugin-svelte": {
"version": "2.46.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz",
"integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"eslint-compat-utils": "^0.5.1",
"esutils": "^2.0.3",
"known-css-properties": "^0.35.0",
"postcss": "^8.4.38",
"postcss-load-config": "^3.1.4",
"postcss-safe-parser": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"semver": "^7.6.2",
"svelte-eslint-parser": "^0.43.0"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0",
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"svelte": {
"optional": true
}
}
},
"node_modules/eslint-scope": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esrap": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.2.tgz",
"integrity": "sha512-FhVlJzvTw7ZLxYZ7RyHwQCFE64dkkpzGNNnphaGCLwjqGk1SQcqzbgdx9FowPCktx6NOSHkzvcZ3vsvdH54YXA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fdir": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz",
"integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/flatted": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true,
"license": "ISC"
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
"integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/known-css-properties": {
"version": "0.35.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
"dev": true,
"license": "MIT"
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true,
"license": "MIT"
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/lightningcss": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
"integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^1.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.29.1",
"lightningcss-darwin-x64": "1.29.1",
"lightningcss-freebsd-x64": "1.29.1",
"lightningcss-linux-arm-gnueabihf": "1.29.1",
"lightningcss-linux-arm64-gnu": "1.29.1",
"lightningcss-linux-arm64-musl": "1.29.1",
"lightningcss-linux-x64-gnu": "1.29.1",
"lightningcss-linux-x64-musl": "1.29.1",
"lightningcss-win32-arm64-msvc": "1.29.1",
"lightningcss-win32-x64-msvc": "1.29.1"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz",
"integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz",
"integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz",
"integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz",
"integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz",
"integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz",
"integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz",
"integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz",
"integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz",
"integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz",
"integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/local-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.0.0.tgz",
"integrity": "sha512-bbgPw/wmroJsil/GgL4qjDzs5YLTBMQ99weRsok1XCDccQeehbHA/I1oRvk2NPtr7KGZgT/Y5tPRnAtMqeG2Kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mlly": "^1.7.3",
"pkg-types": "^1.3.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/mdsvex": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/mdsvex/-/mdsvex-0.12.3.tgz",
"integrity": "sha512-C/uIJamjNo5PHHnR3JHqsBPoLcfUBpzRmAEB6FLMXI/s7XHOceswjDMKqSPEW2WHmYpKm0taZ3U20GSyhMridA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.3",
"prism-svelte": "^0.4.7",
"prismjs": "^1.17.1",
"vfile-message": "^2.0.4"
},
"peerDependencies": {
"svelte": "^3.56.0 || ^4.0.0 || ^5.0.0-next.120"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
}
},
"node_modules/mlly/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-manager-detector": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.9.tgz",
"integrity": "sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==",
"dev": true,
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/pkg-types/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-load-config": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss-safe-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.3.3"
}
},
"node_modules/postcss-scss": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz",
"integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss-scss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.4.29"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz",
"integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
}
},
"node_modules/prism-svelte": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/prism-svelte/-/prism-svelte-0.4.7.tgz",
"integrity": "sha512-yABh19CYbM24V7aS7TuPYRNMqthxwbvx6FF/Rw920YbyBWO3tnyPIqRMgHuSVsLmuHkkBS1Akyof463FVdkeDQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
"integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.30.1",
"@rollup/rollup-android-arm64": "4.30.1",
"@rollup/rollup-darwin-arm64": "4.30.1",
"@rollup/rollup-darwin-x64": "4.30.1",
"@rollup/rollup-freebsd-arm64": "4.30.1",
"@rollup/rollup-freebsd-x64": "4.30.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
"@rollup/rollup-linux-arm-musleabihf": "4.30.1",
"@rollup/rollup-linux-arm64-gnu": "4.30.1",
"@rollup/rollup-linux-arm64-musl": "4.30.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
"@rollup/rollup-linux-riscv64-gnu": "4.30.1",
"@rollup/rollup-linux-s390x-gnu": "4.30.1",
"@rollup/rollup-linux-x64-gnu": "4.30.1",
"@rollup/rollup-linux-x64-musl": "4.30.1",
"@rollup/rollup-win32-arm64-msvc": "4.30.1",
"@rollup/rollup-win32-ia32-msvc": "4.30.1",
"@rollup/rollup-win32-x64-msvc": "4.30.1",
"fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/s-ago": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/s-ago/-/s-ago-2.2.0.tgz",
"integrity": "sha512-t6Q/aFCCJSBf5UUkR/WH0mDHX8EGm2IBQ7nQLobVLsdxOlkryYMbOlwu2D4Cf7jPUp0v1LhfPgvIZNoi9k8lUA==",
"license": "MIT"
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/sass": {
"version": "1.83.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz",
"integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": {
"version": "5.17.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.17.3.tgz",
"integrity": "sha512-eLgtpR2JiTgeuNQRCDcLx35Z7Lu9Qe09GPOz+gvtR9nmIZu5xgFd6oFiLGQlxLD0/u7xVyF5AUkjDVyFHe6Bvw==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.3.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/svelte-check": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.3.tgz",
"integrity": "sha512-IEMoQDH+TrPKwKeIyJim+PU8FxnzQMXsFHR/ldErkHpPXEGHCujHUXiR8jg6qDMqzsif5BbDOUFORltu87ex7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
"integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.3",
"espree": "^9.6.1",
"postcss": "^8.4.39",
"postcss-scss": "^4.0.9"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"svelte": {
"optional": true
}
}
},
"node_modules/svelte-eslint-parser/node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/svelte-eslint-parser/node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/svelte/node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
}
},
"node_modules/tailwindcss": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz",
"integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/terser": {
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true,
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ts-api-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
"integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/unist-util-stringify-position": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
"integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vfile-message": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
"integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vite": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=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"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vitefu": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz",
"integrity": "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==",
"license": "MIT",
"peer": true,
"workspaces": [
"tests/deps/*",
"tests/projects/*"
],
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"license": "MIT"
}
}
}
import eslintPluginSvelte from 'eslint-plugin-svelte';
import * as cssPlugin from 'eslint-plugin-css';
export default [
// add more generic rule sets here, such as:
// js.configs.recommended,
...cssPlugin.configs['flat/standard'],
...eslintPluginSvelte.configs.recommended,
{
rules: {
// override/add rules settings here, such as:
// 'svelte/rule-name': 'error'
}
}
];
{
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
"bracketSameLine": true
}
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
use serde_derive::*;
use std::sync::Arc;
use tokio::sync::mpsc::*;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Update {
Change {
repo: uuid::Uuid,
hash: libpijul::pristine::Hash,
length: u64,
is_tag: bool,
},
Apply {
repo: uuid::Uuid,
channel: String,
hash: libpijul::pristine::Hash,
},
Unrecord {
repo: uuid::Uuid,
channel: String,
hash: libpijul::pristine::Hash,
deps: (),
},
NewChannel {
repo: uuid::Uuid,
channel: String,
},
Fork {
repo: uuid::Uuid,
channel: String,
new: String,
},
Prune {
repo: uuid::Uuid,
channel: String,
},
Rename {
repo: uuid::Uuid,
channel: String,
new: String,
},
}
pub trait Handler: Send + Sync {
type Error: std::fmt::Debug + Send + Sync + 'static;
type F: futures::Future<Output = Result<Option<Self::Error>, Self::Error>> + Send + 'static;
fn update(&self, is_init: bool, update: Update) -> Self::F;
}
#[derive(Clone)]
pub struct Worker<H: Handler> {
pub config: Arc<Config>,
pub handler: Arc<H>,
}
pub struct Config {
pub repositories: PathBuf,
}
impl Config {
pub fn changes_path(&self, id: uuid::Uuid) -> std::path::PathBuf {
let mut p = std::path::Path::new(&self.repositories).join(&format!("{}", id));
p.push(libpijul::DOT_DIR);
p.push("changes");
p
}
pub fn tags_path(&self, id: uuid::Uuid) -> std::path::PathBuf {
self.changes_path(id)
}
}
impl<H: Handler> Worker<H> {
pub fn new(config: Arc<Config>, handler: H) -> Self {
Worker {
config,
handler: Arc::new(handler),
}
}
pub async fn deps(&self, _id: uuid::Uuid) -> Result<(), Error<H::Error>> {
Ok(())
}
pub async fn worker(self, _: UnboundedReceiver<()>) {
futures::future::pending().await
}
pub async fn handle_update(
&self,
_: Option<()>,
_: Option<()>,
_: Option<()>,
update: Update,
) -> Result<(), Error<H::Error>> {
self.handler.update(true, update).await?;
Ok(())
}
pub async fn send_change(
&self,
repo: uuid::Uuid,
hash: libpijul::Hash,
is_tag: bool,
) -> Result<(), Error<H::Error>> {
let mut path = if is_tag {
self.config.tags_path(repo)
} else {
self.config.changes_path(repo)
};
libpijul::changestore::filesystem::push_filename(&mut path, &hash);
if let Ok(m) = std::fs::metadata(&path) {
self.handle_update(
None,
None,
None,
Update::Change {
repo,
hash,
length: m.len(),
is_tag,
},
)
.await?;
Ok(())
} else {
Err(Error::ChangeNotFound { repo, hash })
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigFile {
repositories_path: PathBuf,
}
impl ConfigFile {
pub async fn to_config(self, _blsend: UnboundedSender<()>) -> Config {
Config {
repositories: self.repositories_path,
}
}
}
use thiserror::*;
#[derive(Debug, Error)]
pub enum Error<E> {
#[error(transparent)]
E(#[from]E),
#[error("Change not found")]
ChangeNotFound { repo: uuid::Uuid, hash: libpijul::Hash },
}
[package]
name = "replication"
version = "0.1.0"
authors = ["Pierre-Étienne Meunier <pmeunier@mailbox.org>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "*"
tokio = { version = "1.43", features = [ "net", "time", "rt-multi-thread", "macros", "io-util", "fs", "sync", "signal" ] }
memmap = "*"
log = "*"
env_logger = "*"
serde_derive = "*"
serde = "*"
rand = "0.9"
uuid = { version = "*", features = [ "v4", "serde" ] }
toml = "*"
postgres-types = { version = "*", features = [ "derive" ] }
tokio-postgres = { version = "*", features = [ "with-uuid-1", "with-chrono-0_4" ] }
postgres-openssl = "*"
libpijul = { version = "1.0.0-beta.10", features = [ "tarball" ] }
clap = "~4.5.28"
regex = "*"
lazy_static = "*"
libc = "*"
bytes = "1.10"
deadpool-postgres = "*"
bincode = "*"
thiserror = "*"
webpki = "*"
openssl = "*"
tokio-openssl = "*"
byteorder = "*"
indexmap = "*"
CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
CREATE TYPE keyalgorithm AS ENUM (
'Ed25519'
);
CREATE FUNCTION permissions(uuid, uuid) RETURNS bigint
LANGUAGE plpgsql
AS $_$
DECLARE
perm integer;BEGIN
SELECT coalesce(bit_or(permissions.perm), 0) AS p FROM permissions WHERE
(user_id = $1 OR user_id = '00000000-0000-0000-0000-000000000000') AND repo_id = $2 AND end_date IS NULL
INTO perm;
RETURN perm; END
$_$;
CREATE TABLE users (
id uuid DEFAULT gen_random_uuid() NOT NULL primary key,
login citext unique not null,
password text not null,
email citext unique not null,
email_is_invalid boolean,
creation_date timestamp with time zone not null DEFAULT now(),
is_active boolean not null DEFAULT true,
name text,
creation_ip inet,
last_identity_change timestamp with time zone,
storage_used bigint DEFAULT 0,
suspended boolean not null DEFAULT false
);
-- Dummy user for permissions
INSERT INTO users(id, login, password, email) VALUES('00000000-0000-0000-0000-000000000000', '', '', '');
CREATE TABLE repositories (
id uuid DEFAULT gen_random_uuid() NOT NULL primary key,
owner uuid not null references users(id) on delete cascade,
name text not null,
creation_ip text not null,
fork_origin uuid,
creation_date timestamp with time zone not null DEFAULT now(),
next_discussion_number integer not null DEFAULT 0,
default_channel text not null DEFAULT 'main'::text,
description text,
is_active boolean not null DEFAULT true,
max_changes_size bigint not null DEFAULT '10737418240'::bigint,
n_followers bigint not null DEFAULT 0,
last_pushed timestamp with time zone not null DEFAULT now(),
last_discussed timestamp with time zone not null DEFAULT now(),
rank double precision not null default 0,
UNIQUE(owner, name)
);
CREATE TABLE discussions (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
repository_id uuid not null references repositories(id) on delete cascade,
title text not null,
author uuid references users(id),
creation_ip inet not null,
creation_date timestamp with time zone not null default now(),
number integer not null,
pull_to_branch text,
first_patch integer,
n_changes integer not null default 0,
changes integer not null DEFAULT 0,
closed timestamp with time zone,
uniq bigint not null
);
CREATE TABLE comments (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
discussion_id uuid not null references discussions(id) on delete cascade,
author uuid references users(id),
contents text not null,
creation_ip inet not null,
creation_date timestamp with time zone not null default now(),
cached_html text not null,
uniq bigint not null
);
CREATE TABLE discussion_changes (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
change text not null,
discussion uuid not null references discussions(id) on delete cascade,
pushed_by uuid references users(id),
added timestamp with time zone not null DEFAULT now(),
removed timestamp with time zone
);
CREATE TABLE tags (
id uuid DEFAULT gen_random_uuid() primary key not null,
repository_id uuid not null references repositories(id) on delete cascade,
name text not null,
creation_date timestamp with time zone not null default now(),
deletion_date timestamp with time zone,
color integer not null
);
CREATE TABLE discussion_tags (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
discussion uuid not null references discussions(id) on delete cascade,
-- null: open/closed
tag uuid references tags(id) on delete cascade,
author uuid references users(id),
date timestamp with time zone not null default now(),
active bool not null default true,
addition bool not null default true
);
CREATE TABLE discussion_subscriptions (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
user_id uuid not null references users(id) on delete cascade,
discussion_id uuid not null references discussions(id) on delete cascade,
start_date timestamp with time zone DEFAULT now(),
end_date timestamp with time zone
);
CREATE TABLE signingkeys (
public_key bytea NOT NULL primary key,
user_id uuid not null references users(id) on delete cascade,
algorithm keyalgorithm not null,
signature bytea not null,
expires timestamp with time zone,
added timestamp with time zone not null DEFAULT now()
);
CREATE TABLE contributors (
repo uuid NOT NULL references repositories(id) on delete cascade,
key bytea NOT NULL,
revision timestamp with time zone not null,
primary key(repo, key)
);
CREATE TABLE email_log (
"time" timestamp with time zone primary key not null default now(),
discussion uuid not null references discussions(id) on delete cascade,
email text not null
);
CREATE TABLE hooks (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
repository uuid not null references repositories(id) on delete cascade,
author uuid not null references users(id) on delete cascade,
creation_date timestamp with time zone not null,
active boolean not null DEFAULT true,
secret text not null,
url text not null,
action bigint
);
CREATE TABLE old_logins (
login citext NOT NULL primary key,
user_id uuid,
retired timestamp with time zone DEFAULT now()
);
CREATE TABLE password_failures (
date timestamp with time zone DEFAULT now() primary key not null,
ip inet not null,
login text not null
);
CREATE TABLE permissions (
user_id uuid not null references users(id) ON DELETE CASCADE,
repo_id uuid not null references repositories(id) ON DELETE CASCADE,
perm bigint not null DEFAULT 0,
start_date timestamp with time zone not null DEFAULT now(),
end_date timestamp with time zone,
primary key (user_id, repo_id)
);
CREATE TABLE profile_pics (
user_id uuid NOT NULL references users(id) primary key,
updated timestamp with time zone not null DEFAULT now(),
format text not null,
image bytea not null,
small bytea not null
);
CREATE TABLE publickeys (
id uuid DEFAULT gen_random_uuid() primary key NOT NULL,
user_id uuid not null references users(id) on delete cascade,
publickey text not null,
bin bytea not null,
UNIQUE(id, bin)
);
CREATE TABLE repository_followers (
repository_id uuid not null references repositories(id) on delete cascade,
user_id uuid not null references users(id) on delete cascade,
start_date timestamp with time zone DEFAULT now(),
end_date timestamp with time zone,
primary key(repository_id, user_id)
);
CREATE TABLE tokens (
token bytea NOT NULL primary key,
user_id uuid not null references users(id) on delete cascade,
date timestamp without time zone not null DEFAULT now()
);
GRANT ALL ON TABLE comments TO pijul;
GRANT ALL ON TABLE contributors TO pijul;
GRANT ALL ON TABLE discussion_changes TO pijul;
GRANT ALL ON TABLE discussion_subscriptions TO pijul;
GRANT ALL ON TABLE discussions TO pijul;
GRANT ALL ON TABLE tags TO pijul;
GRANT ALL ON TABLE discussion_tags TO pijul;
GRANT ALL ON TABLE hooks TO pijul;
GRANT ALL ON TABLE old_logins TO pijul;
GRANT ALL ON TABLE password_failures TO pijul;
GRANT ALL ON TABLE permissions TO pijul;
GRANT ALL ON TABLE profile_pics TO pijul;
GRANT ALL ON TABLE publickeys TO pijul;
GRANT ALL ON TABLE repositories TO pijul;
GRANT ALL ON TABLE repository_followers TO pijul;
GRANT ALL ON TABLE signingkeys TO pijul;
GRANT ALL ON TABLE tokens TO pijul;
GRANT ALL ON TABLE users TO pijul;
drop table tokens;
drop table repository_followers;
drop table publickeys;
drop table profile_pics;
drop table permissions;
drop table password_failures;
drop table old_logins;
drop table hooks;
drop table email_log;
drop table contributors;
drop table signingkeys;
drop table discussion_tags;
drop table tags;
drop table discussion_subscriptions;
drop table discussion_changes;
drop table comments;
drop table discussions;
drop table repositories;
drop table users;
drop function permissions;
drop type keyalgorithm;
drop extension pgcrypto;
drop extension pg_trgm;
drop extension citext;
#[macro_use]
extern crate serde_derive;
#[derive(Debug, Serialize, Deserialize)]
pub enum HookContent {
Discussion {
repository_owner: String,
repository_name: String,
discussion_number: u32,
title: String,
author: String,
},
NewChanges {
repository_owner: String,
repository_name: String,
pusher: String,
hash: String,
message: String,
author: Vec<String>,
},
ChangesApplied {
repository_owner: String,
repository_name: String,
applied_by: String,
hash: String,
message: String,
author: Vec<String>,
},
}
# Interacting with the Nest's web hooks
This crate defines the types of hooks that nest.pijul.com can send to your applications.
If your application is written in Rust, this makes parsing easy and reliable.
[package]
name = "pijul-hooks"
description = "Tools to handle webhooks from nest.anu.dev"
version = "0.1.2"
authors = ["Pierre-Étienne Meunier <pe@pijul.org>"]
include = [ "Cargo.toml", "src/lib.rs" ]
license = "MIT/Apache-2.0"
repository = "https://nest.pijul.com/pijul_org/pijul-hooks"
edition = "2021"
[dependencies]
serde = "1.0"
serde_derive = "1.0"
{ pkgs, ...}:
pkgs.stdenv.mkDerivation rec {
name = "geolite2-city";
src = pkgs.fetchurl {
name = "maxmind";
url = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=t587y1OivLCBMc7D&suffix=tar.gz";
hash = "sha256-9E7P/OTn9RixLfmOIozaXeD/Kpdrt7n+5HqY99k5rkI=";
};
buildInputs = [ ];
nativeBuildInputs = [ pkgs.busybox ];
unpackCmd = ''
mkdir src
cp $curSrc src/GeoLite2-City.mmdb.tar.gz
cd src
tar -xf GeoLite2-City.mmdb.tar.gz
find . -exec touch --date=@$SOURCE_DATE_EPOCH {} \;
'';
installPhase = ''
install -m 755 -d $out/share
install -m 644 GeoLite2-City.mmdb $out/share/
'';
meta = {
description = "";
};
}
with import <nixpkgs> {
overlays = map (uri: import (fetchTarball uri)) [
https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz
];
};
let geolite2 = pkgs.callPackage ./geolite2.nix { };
rust = (rustChannelOf { channel = "stable"; }).rust;
stripe = stdenv.mkDerivation rec {
name = "stripe-${version}";
version = "1.5.5";
src = fetchurl {
url = "https://github.com/stripe/stripe-cli/releases/download/v1.5.5/stripe_1.5.5_linux_x86_64.tar.gz";
sha256 = "sha256-kss7cuLS2G7Z/OKBRh7/WNIcVW90Mplhj3elwThZ6KM=";
};
unpackPhase = "tar -xf ${src}";
installPhase = "mkdir -p $out/bin; mv stripe $out/bin";
};
in
clangStdenv.mkDerivation rec {
name = "nest-env";
buildInputs = [
rust openssl pkg-config libsodium
bison flex zstd xxHash
llvmPackages.libclang.lib
stripe
dbmate
sops age
diesel-cli
cargo-edit
];
DIESEL_DATABASE_URL="postgres://postgres@localhost/pijul?sslmode=disable";
LIBCLANG_PATH="${llvmPackages.libclang}/lib";
DATABASE_URL="postgres://postgres@localhost/pijul?sslmode=disable";
LOCAL_DATABASE_URL="postgres://postgres@/pijul-local?sslmode=disable&socket=/var/run/postgresql";
LOCAL_DATABASE_URL2="postgres://postgres@/pijul-local2?sslmode=disable&socket=/var/run/postgresql";
LOCAL_DATABASE_URL3="postgres://postgres@/pijul-local3?sslmode=disable&socket=/var/run/postgresql";
PROTOC="${protobuf}/bin/protoc";
ETCDCTL_API=3;
GEOLITE2_PATH="${geolite2}/share/GeoLite2-City.mmdb";
}
<!-- if let Some(token) = self.token { -->
<div class="p-3">
<!-- if let Some(err) = self.err { -->
<div class="alert alert-warning">{err}</div>
<!-- } -->
<h3>Reset your password</h3>
<div class="p-3">
<form action="/password_reset" class="reset" method="POST">
<div class="row justify-content-center">
<div class="col col-10 col-md-6">
<div class="card">
<div class="card-body">
<div class="mb-3">
<label for="reset_password">Password</label>
<input type="password" id="reset_password" name="reset_password" class="form-control" />
</div>
<div class="mb-3">
<label for="reset_confirm_password">Confirm password</label>
<input type="password" id="reset_confirm" name="reset_confirm_password" class="form-control"/>
</div>
<div class="mb-3">
<input type="hidden" name="token" value="{token}"/>
<button type="submit" name="action" value = "reset" class="btn btn-primary">Reset my password</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- } else { -->
<div class="p-3">
<!-- if let Some(err) = self.err { -->
<div class="alert alert-warning">{err}</div>
<!-- } -->
<h3>Reset your password</h3>
<div class="p-3">
<form action="/password_reset" class="reset" method="POST">
<p>Fill the form below. You'll receive a confirmation email which will allow you to reset your password.</p>
<div class="row justify-content-center">
<div class="col col-10 col-md-6">
<div class="card">
<div class="card-body">
<div class="mb-3">
<label for="reset_login">Login</label>
<input type="text" name="reset_login" placeholder="Login" class="form-control" />
</div>
<div class="mb-3">
<label for="reset_email">Email address</label>
<input type="email" name="reset_email" placeholder="Email address" class="form-control"/>
</div>
<div class="mb-3">
<button type="submit" name="action" value = "reset" class="btn btn-primary">Reset my password</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- } -->
<!--
write!(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">")?;
-->
<html lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<title>
The Pijul Nest
</title>
</head>
<body style="font-family:sans-serif;padding:0.2em 1em;">
<div style="{CONTENT_STYLE}">
<h1>Welcome to The Nest</h1>
<div style="padding: 1em 0;">
<p>
<a style="{A_STYLE}" href="https://{self.host}/register?token={self.token}">Click here to confirm your email address</a>
</p>
</div>
</div>
<div style="{FOOTER_STYLE}">
<p>
—
<br/>
You're receiving this email because you or someone else signed up for an account on <a style="{A_STYLE}" href="https://{self.host}">{self.host}</a>. If you did not sign up yourself, you can safely ignore this email.
We are really happy to have you join the community, and hope you will find us as friendly and enjoyable as we do. Should you have any question, you can open a discussion <a href="https://nest.pijul.com/pijul/pijul">on the Nest</a>, ask it <a href="https://discourse.pijul.org">on our Discourse</a>, come chat with us <a href="https://pijul.zulipchat.com">on Zulip</a>, or reach us directly at <a href="mailto:contact@pijul.org">contact@pijul.org</a>.
<script type="application/ld+json">{
"@context":"http://schema.org","@type":"EmailMessage","action":{
"potentialAction": {
"@type": "ConfirmAction",
"name": "Confirm signup",
"handler": {
"@type": "HttpActionHandler",
"url": "https://{self.host}/register?token={self.token}"
}
},
"description": "Confirm creation of an account on {self.host}."
}
}</script>
</p>
</div>
</body>
</html>
<!--
write!(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">")?;
-->
<html lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>The Pijul Nest</title>
</head>
<body style="font-family: sans-serif; padding: 0.2em 1em">
<div style="{CONTENT_STYLE}">
<h1>Password reset</h1>
<div style="padding: 1em 0">
<p>
Someone (hopefully you) asked to reset your password on {self.host}.
If this wasn't you, you can safely ignore this email. Else, click on
the following link to access your account:
</p>
<p>
<a
style="{A_STYLE}"
href="https://{self.host}/recover?code={self.token}"
>Access your account</a
>
</p>
</div>
</div>
<div style="{FOOTER_STYLE}">
<p>
—
<br />
You're receiving this email because you or someone else for your
password on
<a style="{A_STYLE}" href="https://{self.host}">{self.host}</a> to be
reset. If you did not do this yourself, you can safely ignore this
email.
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "EmailMessage",
"action": {
"potentialAction": {
"@type": "ConfirmAction",
"name": "Confirm password reset",
"handler": {
"@type": "HttpActionHandler",
"url": "https://{self.host}/recover?code={self.token}"
}
},
"description": "Confirm password reset on {self.host}."
}
}
</script>
</p>
</div>
</body>
</html>
<!--
write!(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">")?;
-->
<html lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<title>
The Pijul Nest
</title>
</head>
<body style="font-family:sans-serif;padding:0.2em 1em;">
<div style="{CONTENT_STYLE}">
<p>{ self.author } added a change to #{self.disc} <em>{self.name}</em></p>
<blockquote style="{BLOCKQUOTE_STYLE}">
{self.message}
</blockquote>
<p><a style="{A_STYLE}" href="https://{self.host}/{self.owner}/{self.repo}/discussions/{self.disc}#{self.hash}?login={self.login}">View discussion</a></p>
</div>
<div style="{FOOTER_STYLE}">
<p>
—
<br/>
You're receiving this email because you are subscribed to this discussion. <a style="{A_STYLE}" href="https://{self.host}/{self.owner}/{self.repo}/discussions/{self.disc}/unsubscribe?login={self.login}">Unsubscribe</a>.
<script type="application/ld+json">{
"@context":"http://schema.org","@type":"EmailMessage","action":{
"potentialAction": {
"@type": "ViewAction",
"name": "View Discussion",
"handler": {
"@type": "HttpActionHandler",
"url": "https://{self.host}/{self.owner}/{self.repo}/discussions/{self.disc}?login={self.login}"
}
},
"description": "View discussion."
}
}</script>
</p>
</div>
</body>
</html>
use crate::permissions::Perm;
use crate::repository::{channel_spec, ChannelSpec, RepositoryId};
use crate::Keyalgorithm_;
use byteorder::{BigEndian, ByteOrder};
use cuach::*;
use diesel::sql_types::{BigInt, Bool, Integer, Text};
use diesel::{
BoolExpressionMethods, ExpressionMethods, Insertable, IntoSql, NullableExpressionMethods,
OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncConnection;
use diesel_async::RunQueryDsl;
use futures;
use futures::Future;
use lazy_static::*;
use libpijul::pristine::{Base32, ChannelTxnT, DepsTxnT, GraphTxnT, TxnT};
use libpijul::TxnTExt;
use regex::Regex;
use serde_derive::*;
use std;
use std::collections::HashSet;
use std::io::Read;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use thrussh;
use thrussh::server::*;
use thrussh::ChannelId;
use thrussh_keys;
use tokio::fs::OpenOptions;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tracing::*;
use uuid::Uuid;
pub async fn socket(ssh_addr: &SocketAddr) -> TcpListener {
tokio::net::TcpListener::bind(ssh_addr).await.unwrap()
}
const CHAL_LEN: usize = 30;
lazy_static! {
pub static ref DISC: Regex = Regex::new(r#":([0-9]+)"#).unwrap();
}
pub const EMAIL_CHARSET: &'static str = "UTF-8";
pub async fn worker(config: super::Config, socket: TcpListener) {
loop {
let (socket, addr) = if let Ok(x) = socket.accept().await {
x
} else {
error!("SSH accept");
continue;
};
let ip: std::net::IpAddr = addr.ip();
info!("ssh connection from {:?}", ip);
let config = config.clone();
use crate::db::password_failures::dsl as pf;
let ip_sql = ipnetwork::IpNetwork::new(ip, if ip.is_ipv4() { 32 } else { 128 }).unwrap();
if let (Some(date), count) = pf::password_failures
.filter(pf::ip.eq(ip_sql))
.filter(pf::date.gt(chrono::Utc::now() - chrono::Duration::hours(1)))
.select((diesel::dsl::max(pf::date), diesel::dsl::count(pf::date)))
.get_result::<(Option<chrono::DateTime<chrono::Utc>>, i64)>(
&mut config.db.get().await.unwrap(),
)
.await
.unwrap()
{
let now = chrono::Utc::now();
if now.signed_duration_since(date).num_seconds() < count {
continue;
}
}
tokio::spawn(run_stream(
config.ssh.clone(),
socket,
H {
config: config.clone(),
db: config.db.clone(),
user: String::new(),
user_id: None,
repo: String::new(),
default_channel: String::new(),
channel: None,
repo_id: None,
permissions: crate::permissions::Perm::empty(),
owner: String::new(),
protocol_version: 0,
state: None,
applied: Vec::new(),
ip_sql,
last_window_adjustment: SystemTime::now(),
random_challenge: RandomChallenge {
ch: String::new(),
gen: std::time::SystemTime::UNIX_EPOCH,
key: None,
},
},
));
}
}
struct H {
pub config: super::Config,
pub db: crate::config::Db,
pub protocol_version: usize,
pub user: String,
pub user_id: Option<Uuid>,
pub owner: String,
pub repo: String,
pub permissions: crate::permissions::Perm,
pub channel: Option<ChannelSpec>,
pub default_channel: String,
pub repo_id: Option<RepositoryId>,
pub state: Option<State>,
pub ip_sql: ipnetwork::IpNetwork,
pub applied: Vec<libpijul::pristine::Hash>,
pub last_window_adjustment: SystemTime,
pub random_challenge: RandomChallenge,
}
struct RandomChallenge {
ch: String,
gen: std::time::SystemTime,
key: Option<libpijul::key::PublicKey>,
}
struct TmpFile {
f: Option<tokio::fs::File>,
path: std::path::PathBuf,
}
impl TmpFile {
async fn persist(
mut self,
path: &std::path::Path,
) -> Result<tokio::fs::File, tokio::io::Error> {
tokio::fs::rename(&self.path, path).await?;
Ok(self.f.take().unwrap())
}
}
impl Drop for TmpFile {
fn drop(&mut self) {
if self.f.is_some() {
debug!("dropping file {:?}", self.path);
std::fs::remove_file(&self.path).unwrap_or(());
}
}
}
enum State {
Protocol,
Change {
hash: libpijul::pristine::Hash,
size: usize,
file: TmpFile,
file_size: usize,
},
}
impl From<libpijul::key::Algorithm> for Keyalgorithm_ {
fn from(a: libpijul::key::Algorithm) -> Keyalgorithm_ {
match a {
libpijul::key::Algorithm::Ed25519 => Keyalgorithm_::Ed25519,
}
}
}
impl From<Keyalgorithm_> for libpijul::key::Algorithm {
fn from(a: Keyalgorithm_) -> libpijul::key::Algorithm {
match a {
Keyalgorithm_::Ed25519 => libpijul::key::Algorithm::Ed25519,
}
}
}
impl H {
async fn reject(self) -> Result<(Self, thrussh::server::Auth), crate::Error> {
use crate::db::password_failures::dsl as pf;
diesel::insert_into(pf::password_failures)
.values((pf::ip.eq(&self.ip_sql), pf::login.eq(&self.user)))
.execute(&mut self.db.get().await.unwrap())
.await?;
Ok((self, thrussh::server::Auth::Reject))
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Signature {
public_key: String,
timestamp: chrono::DateTime<chrono::Utc>,
signature: String,
}
impl Handler for H {
type Error = crate::Error;
type FutureAuth = Pin<Box<dyn Future<Output = Result<(Self, Auth), crate::Error>> + Send>>;
type FutureUnit = Pin<Box<dyn Future<Output = Result<(Self, Session), crate::Error>> + Send>>;
type FutureBool = futures::future::Ready<Result<(Self, Session, bool), crate::Error>>;
fn finished(self, session: thrussh::server::Session) -> Self::FutureUnit {
Box::pin(futures::future::ready(Ok((self, session))))
}
fn finished_auth(self, auth: thrussh::server::Auth) -> Self::FutureAuth {
Box::pin(futures::future::ready(Ok((self, auth))))
}
fn finished_bool(self, b: bool, s: thrussh::server::Session) -> Self::FutureBool {
futures::future::ready(Ok((self, s, b)))
}
fn auth_publickey(
mut self,
user: &str,
public_key: &thrussh_keys::key::PublicKey,
) -> Self::FutureAuth {
self.user.clear();
self.user.push_str(user);
use thrussh_keys::PublicKeyBase64;
debug!("auth_publickey {:?}", public_key);
debug!("user {:?}", user);
let key_bytes = match public_key {
thrussh_keys::key::PublicKey::RSA { ref key, .. } => {
(thrussh_keys::key::PublicKey::RSA {
key: thrussh_keys::key::OpenSSLPKey(key.0.clone()),
hash: thrussh_keys::key::SignatureHash::SHA2_512,
})
.public_key_bytes()
}
k => k.public_key_bytes(),
};
debug!(
"key_bytes = {:?}",
data_encoding::HEXLOWER.encode(&key_bytes)
);
Box::pin(async move {
use crate::db::publickeys::dsl as publickeys;
use crate::db::users::dsl as users;
let mut db_ = self.db.get().await?;
if let Some(id) = users::users
.inner_join(publickeys::publickeys)
.filter(users::login.eq(&self.user))
.filter(publickeys::bin.eq(&key_bytes))
.filter(users::is_active)
.select(users::id)
.get_result(&mut db_)
.await
.optional()?
{
debug!("accept");
use crate::db::password_failures::dsl as pf;
diesel::delete(pf::password_failures)
.filter(pf::ip.eq(self.ip_sql))
.filter(pf::login.eq(&self.user))
.execute(&mut db_)
.await?;
self.user_id = Some(id);
Ok((self, Auth::Accept))
} else {
std::mem::drop(db_);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
Ok((self, Auth::Reject))
}
})
}
fn auth_password(mut self, user: &str, password: &str) -> Self::FutureAuth {
self.user.clear();
self.user.push_str(user);
let password = password.to_string();
Box::pin(async move {
use crate::db::password_failures::dsl as pf;
let mut db_ = self.db.get().await?;
let count = pf::password_failures
.select(diesel::dsl::count::<BigInt, i64>(1))
.filter(pf::ip.eq(self.ip_sql).or(pf::login.eq(&self.user)))
.get_result::<i64>(&mut db_)
.await?;
if count > self.config.max_password_attempts {
return Ok((self, Auth::Reject));
}
use crate::db::users::dsl as users;
if let Some(user_id) = users::users
.filter(users::login.eq(&self.user))
.filter(crate::check_password!(&password))
.filter(users::email_is_invalid.is_null())
.filter(users::is_active)
.select(users::id)
.get_result(&mut db_)
.await
.optional()?
{
self.user_id = Some(user_id);
return Ok((self, Auth::Accept));
} else {
self.reject().await
}
})
}
fn exec_request(
mut self,
ch: thrussh::ChannelId,
exec: &[u8],
mut session: Session,
) -> Self::FutureUnit {
debug!("exec_request = {:?}", exec);
let exec = if let Ok(exec) = std::str::from_utf8(exec) {
exec.to_string()
} else {
return Box::pin(async { Ok((self, session)) });
};
debug!("exec: {:?}", exec);
Box::pin(async move {
let mut words = exec.split_whitespace();
if !self.parse_request(&exec) {
return Ok((self, session));
}
if let (Some("pijul"), Some(cmd)) = (words.next(), words.next()) {
match cmd {
"protocol" => {
debug!(
"owner = {:?}, repo = {:?}, channel = {:?}",
self.owner, self.repo, self.channel
);
if self.repo.is_empty() {
self.state = Some(State::Protocol);
session.channel_success(ch);
return Ok((self, session));
}
use crate::db::repositories::dsl as r;
use crate::db::users::dsl as users;
if let Some((repo_id, owner_id, fork_origin, permissions, channel, suspended, public)) = r::repositories
.inner_join(users::users)
.filter(users::login.eq(&self.owner))
.filter(r::name.eq(&self.repo))
.filter(r::is_active)
.filter(users::is_active)
.select((
r::id,
users::id,
r::fork_origin,
crate::permissions!(self.user_id.unwrap(), r::id),
r::default_channel,
users::suspended,
crate::repository::is_public(),
))
.get_result::<(uuid::Uuid, uuid::Uuid, Option<uuid::Uuid>, i64, String, bool, bool)>(&mut self.db.get().await?)
.await.optional()?
{
self.repo_id = Some(RepositoryId {
repo_id,
owner_id,
fork_origin,
});
self.permissions = Perm::from_bits(permissions).unwrap();
let channel: String = channel;
let suspended: bool = suspended;
let public: bool = public;
if !public && suspended {
self.permissions &= Perm::READ
}
self.default_channel = channel.clone();
self.channel = Some(ChannelSpec::channel(channel.into()));
self.state = Some(State::Protocol);
session.channel_success(ch);
Ok((self, session))
} else {
debug!("permission denied");
session.extended_data(
ch,
1,
thrussh::CryptoVec::from_slice(
b"Repository not found, or insufficient permissions\n",
),
);
session.exit_status_request(ch, 1);
Ok((self, session))
}
}
cmd => {
error!("cmd = {:?}", cmd);
session.extended_data(
ch,
1,
thrussh::CryptoVec::from_slice(b"Invalid Pijul subcommand\n"),
);
session.exit_status_request(ch, 1);
Ok((self, session))
}
}
} else {
session.exit_status_request(ch, 127);
Ok((self, session))
}
})
}
fn data(
mut self,
chan: thrussh::ChannelId,
data: &[u8],
mut session: Session,
) -> Self::FutureUnit {
debug!("data len {:?}", data.len());
let data = data.to_vec();
Box::pin(async move {
let result = self.protocol(chan, &data, &mut session).await;
match result {
Ok(ProtocolResult::Change { hash, file, size }) => {
let result = self.apply(hash, file, size, chan, &mut session).await;
debug!("result: {:?}", result);
if let Err(e) = result {
session.extended_data(
chan,
1,
thrussh::CryptoVec::from_slice(e.to_string().as_bytes()),
);
session.exit_status_request(chan, 1);
return Ok((self, session));
}
self.applied.push(hash);
}
Ok(ProtocolResult::ChannelList(disc)) => {
if self.repo_id.is_none() {
return Ok((self, session));
}
let repo = self
.config
.repo_locks
.get(&self.repo_id.as_ref().unwrap().origin_id())
.await?;
let data = crate::discussions::discussion_changelist(
&mut self.db.get().await.unwrap(),
&repo.changes,
self.repo_id.as_ref().unwrap().repo_id,
disc,
)
.await?;
session.data(chan, thrussh::CryptoVec::from_slice(&data));
self.state = Some(State::Protocol);
}
Ok(ProtocolResult::State { disc, pos }) => {
if self.repo_id.is_none() {
return Ok((self, session));
}
let (n, m) = crate::discussions::discussion_state(
&mut self.db.get().await.unwrap(),
self.repo_id.as_ref().unwrap().repo_id,
disc,
pos,
)
.await?;
let data = format!("{} {}\n", n, m.to_base32());
session.data(chan, thrussh::CryptoVec::from_slice(&data.as_bytes()));
self.state = Some(State::Protocol);
}
Ok(ProtocolResult::None) => {}
Err(e) => {
match e {
crate::Error::AmbiguousInode => {
session.extended_data(
chan,
1,
thrussh::CryptoVec::from_slice(b"Ambiguous path\n"),
);
}
crate::Error::InodeNotFound => {
session.extended_data(
chan,
1,
thrussh::CryptoVec::from_slice(b"Path not found\n"),
);
}
_ => {
session.extended_data(
chan,
1,
thrussh::CryptoVec::from_slice(b"Channel not found or empty\n"),
);
}
}
session.eof(chan);
session.exit_status_request(chan, 1);
session.close(chan);
return Ok((self, session));
}
}
Ok((self, session))
})
}
fn adjust_window(&mut self, _channel: ChannelId, target: u32) -> u32 {
let elapsed = self.last_window_adjustment.elapsed().unwrap();
self.last_window_adjustment = SystemTime::now();
if target >= 10_000_000 {
return target;
}
if elapsed < Duration::from_secs(2) {
target * 2
} else if elapsed > Duration::from_secs(8) {
target / 2
} else {
target
}
}
fn channel_eof(self, channel: ChannelId, mut session: Session) -> Self::FutureUnit {
debug!("channel eof");
debug!("{:#?}", self.applied);
Box::pin(async move {
if self.repo_id.is_none() {
return Ok((self, session));
}
session.eof(channel);
session.exit_status_request(channel, 0);
session.close(channel);
Ok((self, session))
})
}
}
enum ProtocolResult {
None,
Change {
hash: libpijul::pristine::Hash,
file: TmpFile,
size: usize,
},
ChannelList(i32),
State {
disc: i32,
pos: Option<u64>,
},
}
enum ChannelList {
Discussion(i32),
Data(Vec<u8>),
}
impl H {
fn parse_request<'a>(&mut self, exec: &'a str) -> bool {
lazy_static! {
static ref ARGS: Regex =
Regex::new(r#"--(\S*) +(("((\\\\|\\"|[^"])*)")|((\\\\|\\ |[^ ])*))"#).unwrap();
static ref REPO: Regex = Regex::new(r#"'?/?(([^/']*)/)?([^']+)'?"#).unwrap();
};
for cap in ARGS.captures_iter(exec) {
debug!("cap:{:?}", cap);
match cap.get(1).as_ref().map(|x| x.as_str()) {
Some("repository") => {
if let Some(repo_) = cap.get(4).or(cap.get(6)) {
for cap in REPO.captures_iter(repo_.as_str()) {
debug!("cap {:?}", cap);
if let Some(owner) = cap.get(2) {
self.owner.clear();
self.owner.push_str(owner.as_str())
} else {
self.owner.clear();
self.owner.push_str(&self.user)
}
self.repo.clear();
self.repo.push_str(cap.get(3).unwrap().as_str());
}
} else {
error!("unparsed command: {:?}", exec);
return false;
}
}
Some("version") => {
if let Some(ver) = cap.get(4).or(cap.get(6)) {
if ver.as_str() == "3" {
self.protocol_version = 3
} else {
return false;
}
} else {
return false;
}
}
_ => {
debug!("extra: {:?}", cap);
}
}
}
self.protocol_version == 3
}
async fn protocol(
&mut self,
chan: thrussh::ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<ProtocolResult, crate::Error> {
lazy_static! {
static ref STATE: Regex = Regex::new(r#"^state\s+(\S+)(\s+([0-9]+)?)\s+"#).unwrap();
static ref ID: Regex = Regex::new(r#"^id\s+(\S+)\s+"#).unwrap();
static ref CHANGELIST: Regex =
Regex::new(r#"^changelist\s+(\S+)\s+([0-9]+)(.*)\s+"#).unwrap();
static ref CHANGELIST_PATHS: Regex = Regex::new(r#""((\\")|[^"])+""#).unwrap();
static ref CHANGE: Regex = Regex::new(r#"^((change)|(partial))\s+([^ ]*)\s+"#).unwrap();
static ref APPLY: Regex =
Regex::new(r#"^apply\s+(\S+)\s+([^ ]*) ([0-9]+)\s+"#).unwrap();
static ref TAG: Regex = Regex::new(r#"^tag\s+(\S+)\s+"#).unwrap();
static ref TAGUP: Regex =
Regex::new(r#"^tagup\s+(\S+)\s+(\S+)\s+([0-9]+)\s+"#).unwrap();
static ref CHANNEL: Regex = Regex::new(r#"^channel\s+(\S+)\s+"#).unwrap();
static ref ARCHIVE: Regex =
Regex::new(r#"^archive\s+(\S+)\s*(( ([^:]+))*)( :(.*))?\n"#).unwrap();
static ref CHALLENGE: Regex = Regex::new(r#"^challenge (\S+)\n"#).unwrap();
static ref IDENTITIES: Regex = Regex::new(r#"^identities( (\d+))?\n"#).unwrap();
static ref PROVE: Regex = Regex::new(r#"^prove (.+)\n"#).unwrap();
}
debug!("protocol, channel {:?}", self.channel);
match self.state.take() {
Some(State::Protocol) => {
let data = {
if let Some(n) = data.iter().position(|&x| x == b'\n') {
&data[..n + 1]
} else {
return Err(crate::Error::ProtocolError.into());
}
};
let data = std::str::from_utf8(data)?;
debug!("data = {:?}", data);
if let Some(cap) = CHANGELIST.captures(data) {
match self.changelist(cap).await? {
ChannelList::Discussion(d) => return Ok(ProtocolResult::ChannelList(d)),
ChannelList::Data(data) => {
trace!("sending data {:?}", data);
session.data(chan, thrussh::CryptoVec::from_slice(&data));
debug!("sent");
self.state = Some(State::Protocol);
}
}
} else if let Some(cap) = STATE.captures(data) {
match self.state(cap).await? {
(ChannelList::Discussion(disc), pos) => {
return Ok(ProtocolResult::State { disc, pos })
}
(ChannelList::Data(data), _) => {
trace!("sending data {:?}", data);
session.data(chan, thrussh::CryptoVec::from_slice(&data));
debug!("sent");
self.state = Some(State::Protocol);
}
}
} else if let Some(cap) = ID.captures(data) {
match self.id(cap, false).await? {
ChannelList::Discussion(_) => {
session.data(chan, thrussh::CryptoVec::from_slice(b"\n"));
}
ChannelList::Data(data) => {
session.data(chan, thrussh::CryptoVec::from_slice(&data));
}
}
self.state = Some(State::Protocol);
} else if let Some(cap) = APPLY.captures(data) {
self.parse_apply(cap).await?;
} else if let Some(cap) = CHANGE.captures(data) {
let is_change = &cap[1] == "change";
self.parse_change(cap, chan, session, is_change)?;
self.state = Some(State::Protocol);
} else if let Some(cap) = CHALLENGE.captures(data) {
self.challenge(cap, chan, session)?;
self.state = Some(State::Protocol);
} else if let Some(cap) = PROVE.captures(data) {
self.prove(cap, chan, session).await?;
self.state = Some(State::Protocol);
} else if let Some(cap) = IDENTITIES.captures(data) {
let id: Option<i64> = if let Some(id) = cap.get(2) {
Some(id.as_str().parse()?)
} else {
None
};
self.identities(id, chan, session).await?;
self.state = Some(State::Protocol);
} else {
debug!("not recognised: {:?}", data);
self.state = Some(State::Protocol);
return Ok(ProtocolResult::None);
}
}
Some(State::Change {
hash,
size,
mut file,
mut file_size,
}) => {
debug!("data.len() = {:?}", data.len());
file.f.as_mut().unwrap().write_all(data).await?;
file_size += data.len();
debug!("{:?} < {:?}", file_size, size);
if file_size < size {
self.state = Some(State::Change {
hash,
size,
file,
file_size,
})
} else {
self.state = Some(State::Protocol);
return Ok(ProtocolResult::Change { hash, file, size });
}
}
None => {}
}
Ok(ProtocolResult::None)
}
fn challenge<'a>(
&mut self,
cap: regex::Captures<'a>,
chan: ChannelId,
session: &mut Session,
) -> Result<(), crate::Error> {
use rand::Rng;
if let Ok(json) = serde_json::from_slice::<libpijul::key::PublicKey>(cap[1].as_bytes()) {
self.random_challenge.key = Some(json)
} else {
return Ok(());
};
self.random_challenge.ch.clear();
self.random_challenge.ch.reserve(CHAL_LEN);
self.random_challenge.ch.extend(
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(30)
.map(char::from),
);
self.random_challenge.ch.push('\n');
session.data(
chan,
thrussh::CryptoVec::from_slice(self.random_challenge.ch.as_bytes()),
);
self.random_challenge.ch.pop();
self.random_challenge.gen = std::time::SystemTime::now();
Ok(())
}
async fn identities(
&mut self,
_rev: Option<i64>,
chan: ChannelId,
session: &mut Session,
) -> Result<(), crate::Error> {
// Not well-supported yet.
if let Some(ref _id) = self.repo_id {
session.data(chan, thrussh::CryptoVec::from_slice(b"\n"));
}
Ok(())
}
async fn prove<'a>(
&mut self,
cap: regex::Captures<'a>,
chan: ChannelId,
session: &mut Session,
) -> Result<(), crate::Error> {
if self.random_challenge.gen.elapsed()? > std::time::Duration::from_secs(10) {
session.data(
chan,
thrussh::CryptoVec::from_slice(b"Challenge expired.\n"),
);
return Ok(());
}
if let Some(ref key) = self.random_challenge.key {
let pk = key.load()?;
debug!("challenge {:?}", self.random_challenge.ch);
pk.verify(
self.random_challenge.ch.as_bytes(),
&cap[1],
&chrono::Utc::now(),
)?;
let key_key = bs58::decode(&key.key).into_vec()?;
let key_sig = bs58::decode(&key.signature).into_vec()?;
use crate::db::signingkeys::dsl as sk;
diesel::insert_into(sk::signingkeys)
.values((
sk::user_id.eq(self.user_id.as_ref().unwrap()),
sk::algorithm.eq(Keyalgorithm_::from(key.algorithm)),
sk::public_key.eq(key_key),
sk::signature.eq(key_sig),
sk::expires.eq(key.expires),
))
.execute(&mut self.db.get().await?)
.await?;
session.data(chan, thrussh::CryptoVec::from_slice(b"\n"));
}
Ok(())
}
async fn changelist<'a>(
&mut self,
cap: regex::Captures<'a>,
) -> Result<ChannelList, crate::Error> {
let from: u64 = (&cap[2]).parse()?;
lazy_static! {
static ref CHANGELIST_PATHS: Regex = Regex::new(r#""(((\\")|[^"])+)""#).unwrap();
}
if let Some(disc) = DISC.captures(&cap[1]) {
let disc: i32 = disc[1].parse().unwrap();
Ok(ChannelList::Discussion(disc))
} else if let Some(ref id) = self.repo_id {
debug!("taking lock");
let repo = self.config.repo_locks.get(&id.origin_id()).await?;
let pristine = repo.pristine.read().await;
let txn = pristine.txn_begin()?;
let c = channel_spec(id, &cap[1]);
let channel_spec = ChannelSpec::channel((&cap[1]).into());
let channel = if let Some(channel) = txn.load_channel(&c)? {
channel
} else if &cap[1] == self.default_channel {
debug!("channel not found {:?}", c);
match channel_spec {
ChannelSpec::Channel(ref c_) => {
debug!("c_ = {:?} {:?}", c_, &cap[1]);
if c_ == &cap[1] {
return Ok(ChannelList::Data(vec![b'\n']));
}
}
ChannelSpec::CI(ref c_) => {
if c_.len() + 3 == cap[1].len() {
let (a, b) = cap[1].split_at(cap[1].len() - 3);
if c_ == a && b == "/ci" {
return Ok(ChannelList::Data(vec![b'\n']));
}
}
}
_ => {}
}
return Err((crate::Error::ChannelNotFound {
channel: cap[1].to_string(),
})
.into());
} else {
return Ok(ChannelList::Data(
format!("error:Remote channel {:?} not found\n", &cap[1]).into(),
));
};
let mut paths = HashSet::new();
let mut data = Vec::new();
for r in CHANGELIST_PATHS.captures_iter(&cap[3]) {
if let Ok((p, ambiguous)) = txn.follow_oldest_path(&repo.changes, &channel, &r[1]) {
let h = txn.get_external(&p.change)?.unwrap();
let h: libpijul::Hash = h.into();
writeln!(data, "{}.{}", h.to_base32(), p.pos.0)?;
if ambiguous {
return Err(crate::Error::AmbiguousInode.into());
}
paths.insert(p);
paths.extend(
libpijul::fs::iter_graph_descendants(&txn, txn.graph(&*channel.read()), p)?
.filter_map(|x| x.ok()),
);
} else {
return Err(crate::Error::InodeNotFound.into());
}
}
use std::io::Write;
let tags: Vec<u64> = txn
.iter_tags(txn.tags(&*channel.read()), from)?
.map(|k| (*k.unwrap().0).into())
.collect();
let mut tagsi = 0;
for x in txn.log(&*channel.read(), from)? {
let (n, (h, m)) = x?;
let h_int = txn.get_internal(h)?.unwrap();
let mut on_channel = paths.is_empty();
if !on_channel {
let mut it = paths.iter();
loop {
if let Some(x) = it.next() {
if x.change == *h_int
|| txn.get_touched_files(x, Some(h_int))?.is_some()
{
on_channel = true;
break;
}
} else {
break;
}
}
}
if on_channel {
let h: libpijul::Hash = h.into();
let m: libpijul::Merkle = m.into();
if tags.get(tagsi) == Some(&n) {
writeln!(data, "{}.{}.{}.", n, h.to_base32(), m.to_base32())?;
tagsi += 1
} else {
writeln!(data, "{}.{}.{}", n, h.to_base32(), m.to_base32())?;
}
}
}
data.push(b'\n');
Ok(ChannelList::Data(data))
} else {
debug!("no repo id / no channel");
Ok(ChannelList::Data(Vec::new()))
}
}
async fn state<'a>(
&mut self,
cap: regex::Captures<'a>,
) -> Result<(ChannelList, Option<u64>), crate::Error> {
let pos: Option<u64> = cap.get(3).map(|c| c.as_str().parse().unwrap());
if let Some(disc) = DISC.captures(&cap[1]) {
let disc: i32 = disc[1].parse().unwrap();
Ok((ChannelList::Discussion(disc), pos))
} else if let Some(ref id) = self.repo_id {
debug!("taking lock");
let repo = self
.config
.repo_locks
.get(&self.repo_id.as_ref().unwrap().origin_id())
.await?;
let pristine = repo.pristine.read().await;
let txn = pristine.txn_begin()?;
let c = channel_spec(id, &cap[1]);
let channel = if let Some(channel) = txn.load_channel(&c)? {
channel
} else {
debug!("channel not found {:?}", c);
return Ok((ChannelList::Data(vec![b'\n']), pos));
};
let mut data = Vec::new();
use std::io::Write;
if let Some(pos) = pos {
for n in txn.log(&*channel.read(), pos)? {
let (n, (_, m)) = n?;
if n < pos {
continue;
} else if n > pos {
data.push(b'\n');
break;
} else {
let m: libpijul::Merkle = m.into();
let m2 = if let Some(x) = txn
.rev_iter_tags(txn.tags(&*channel.read()), Some(n))?
.next()
{
x?.1.b.into()
} else {
libpijul::Merkle::zero()
};
writeln!(&mut data, "{} {} {}", n, m.to_base32(), m2.to_base32()).unwrap();
break;
}
}
} else {
if let Some(n) = txn.reverse_log(&*channel.read(), None)?.next() {
let (n, (_, m)) = n?;
let m: libpijul::Merkle = m.into();
let m2 = if let Some(x) = txn
.rev_iter_tags(txn.tags(&*channel.read()), Some(n))?
.next()
{
x?.1.b.into()
} else {
libpijul::Merkle::zero()
};
writeln!(data, "{} {} {}", n, m.to_base32(), m2.to_base32())?
} else {
writeln!(data, "-")?;
}
}
if data.is_empty() {
data.push(b'\n')
}
Ok((ChannelList::Data(data), pos))
} else {
debug!("no repo id / no channel");
Ok((ChannelList::Data(Vec::new()), pos))
}
}
async fn id<'a>(
&mut self,
cap: regex::Captures<'a>,
is_tag: bool,
) -> Result<ChannelList, crate::Error> {
debug!("id is_tag {:?}", is_tag);
if let Some(disc) = DISC.captures(&cap[1]) {
let disc: i32 = disc[1].parse().unwrap();
return Ok(ChannelList::Discussion(disc));
} else if let Some(ref id) = self.repo_id {
debug!("taking lock");
let repo = self
.config
.repo_locks
.get(&self.repo_id.as_ref().unwrap().origin_id())
.await?;
debug!("ok, reading");
let pristine = repo.pristine.read().await;
debug!("ok, reading");
let txn = pristine.txn_begin()?;
debug!("ok, reading");
let c = channel_spec(id, &cap[1]);
debug!("channel c = {:?}", c);
if let (Some(ChannelSpec::Channel(ref c)), Some(ref id)) =
(&self.channel, &self.repo_id)
{
if c == &self.default_channel {
let id = if is_tag {
let mut id_ = [0; 16];
for (a, b) in id_.iter_mut().zip(id.repo_id.as_bytes().iter()) {
*a = 255 - *b
}
data_encoding::BASE32_NOPAD.encode(&id_)
} else {
data_encoding::BASE32_NOPAD.encode(id.repo_id.as_bytes())
};
debug!("id {:?} = {:?}", is_tag, id);
return Ok(ChannelList::Data(format!("{}\n", id).into()));
}
}
if let Some(channel) = txn.load_channel(&c)? {
let resp = format!(
"{}\n",
data_encoding::BASE32_NOPAD.encode(channel.read().id.as_bytes())
)
.into();
debug!("resp = {:?}", resp);
return Ok(ChannelList::Data(resp));
}
}
debug!("no repo id / no channel");
Ok(ChannelList::Data(b"\n".to_vec()))
}
async fn parse_apply<'a>(&mut self, cap: regex::Captures<'a>) -> Result<(), crate::Error> {
let hash = if let Some(h) = libpijul::pristine::Hash::from_base32((&cap[2]).as_bytes()) {
h
} else {
debug!("wrong hash");
self.state = Some(State::Protocol);
return Ok(());
};
if let Ok(d) = cap[1].parse::<i32>() {
self.channel = Some(ChannelSpec::Discussion(d))
} else {
self.channel = Some(ChannelSpec::channel(cap[1].into()));
};
let size: usize = (&cap[3]).parse()?;
debug!("parse_apply = {:?} {:?}", hash, size);
let mut path = self.config.repositories_path.join("tmp");
path.push(
self.repo_id
.as_ref()
.map(|r| r.repo_id)
.unwrap()
.hyphenated()
.to_string(),
);
tokio::fs::create_dir_all(&path).await?;
path.push(&hash.to_base32());
let f = Some(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)
.await?,
);
let file = TmpFile { f, path };
self.state = Some(State::Change {
hash,
size,
file_size: 0,
file,
});
Ok(())
}
fn parse_change(
&mut self,
cap: regex::Captures,
chan: ChannelId,
session: &mut Session,
full: bool,
) -> Result<(), crate::Error> {
let hash = if let Some(h) = libpijul::pristine::Hash::from_base32((&cap[4]).as_bytes()) {
h
} else {
debug!("wrong hash");
return Ok(());
};
if self.repo_id.is_none() {
return Ok(());
}
let mut p = self
.repo_id
.as_ref()
.unwrap()
.nest_changes_path(&self.config);
libpijul::changestore::filesystem::push_filename(&mut p, &hash);
debug!("change path = {:?}", p);
let mut v = thrussh::CryptoVec::new();
let mut f = std::fs::File::open(&p)?;
let full_size = std::fs::metadata(&p)?.len();
let size = if full || full_size <= self.config.partial_change_size {
full_size
} else {
libpijul::change::Change::size_no_contents(&mut f)?
};
v.resize(8 + size as usize);
f.read_exact(&mut v[8..])?;
BigEndian::write_u64(&mut v[..8], size);
debug!("sending {:?}", size);
session.data(chan, v);
Ok(())
}
async fn apply(
&mut self,
hash: libpijul::pristine::Hash,
file: TmpFile,
size: usize,
channel_id: thrussh::ChannelId,
session: &mut thrussh::server::Session,
) -> Result<(), crate::Error> {
// Check the hash, and save the change.
debug!("saving change {:?}", hash);
// Apply
debug!("apply: {:?} {:?}", self.repo_id, self.channel);
if self.repo_id.is_none() {
return Ok(());
}
match self.channel {
Some(ChannelSpec::CI(_)) | Some(ChannelSpec::Channel(_)) => {
if !self.permissions.contains(Perm::APPLY) {
return Err((crate::Error::Forbidden).into());
}
}
Some(ChannelSpec::Discussion(_)) => {
if !self.permissions.contains(Perm::CREATE_DISCUSSION) {
return Err((crate::Error::Forbidden).into());
}
}
None => return Ok(()),
}
let channel = self.channel.take().unwrap();
debug!("applying {:?} to {:?}", hash, channel);
let repo_id = self.repo_id.as_ref().unwrap();
let repo = self
.config
.repo_locks
.get(&repo_id.origin_id())
.await
.unwrap();
if !repo.changes.has_change(&hash) {
let owner = self.repo_id.as_ref().unwrap().owner_id.clone();
use crate::db::users::dsl as u;
let n = if self.config.size_limit > 0 {
diesel::update(u::users.find(owner))
.set((u::storage_used.eq(u::storage_used + size as i64),))
.filter(
(u::storage_used + size as i64).le(self.config.size_limit)
)
.execute(&mut self.db.get().await?)
.await?
} else {
diesel::update(u::users.find(owner))
.set((u::storage_used.eq(u::storage_used + size as i64),))
.execute(&mut self.db.get().await?)
.await?
};
if n > 0 {
let name = repo.changes.filename(&hash);
tokio::fs::create_dir_all(name.parent().unwrap()).await?;
let mut file = file.persist(&name).await?;
debug!("seek");
let changes = repo.changes.clone();
file.flush().await?;
file.seek(tokio::io::SeekFrom::Start(0)).await?;
debug!("seek done");
changes.check(&mut file, &hash, None).await?;
} else {
return Err((crate::Error::Quota { quota: 0 }).into());
}
}
match channel {
ChannelSpec::Channel(c) => self.apply_to_channel(repo, c, hash).await?,
ChannelSpec::CI(c) => self.channel = Some(ChannelSpec::CI(c)),
ChannelSpec::Discussion(d) => {
self.apply_to_discussion(repo, d, hash, channel_id, session)
.await?
}
}
Ok(())
}
async fn apply_to_channel(
&mut self,
repo: Arc<crate::repository::Repo>,
c: String,
hash: libpijul::Hash,
) -> Result<(), crate::Error> {
use libpijul::changestore::ChangeStore;
let header = repo.changes.get_header(&hash)?;
let id = self.repo_id.as_ref().unwrap();
let repo_id = self.repo_id.as_ref().unwrap().repo_id;
self.config
.replicator
.send_change(id.origin_id(), hash, false)
.await?;
self.config
.replicator
.handle_update(
None,
None,
None,
::replication::Update::Apply {
repo: id.origin_id(),
channel: c.clone(),
hash,
},
)
.await?;
let hook = pijul_hooks::HookContent::ChangesApplied {
repository_owner: self.owner.to_string(),
repository_name: self.repo.to_string(),
applied_by: self.user.to_string(),
message: header.message.clone(),
author: crate::change::authors_string(&self.db, &header.authors).await?,
hash: hash.to_base32(),
};
let mut db_ = self.db.get().await?;
use crate::db::repositories::dsl as repos;
diesel::update(repos::repositories.find(repo_id))
.set(repos::last_pushed.eq(diesel::dsl::now))
.execute(&mut db_)
.await?;
for h in header.authors {
debug!("author {:?}", h);
if let Some(k) =
h.0.get("key")
.and_then(|k| bs58::decode(k.as_bytes()).into_vec().ok())
{
use crate::db::contributors::dsl as c;
diesel::insert_into(c::contributors)
.values((
c::repo.eq(repo_id),
c::key.eq(k),
c::revision.eq(diesel::dsl::now),
))
.on_conflict_do_nothing()
.execute(&mut db_)
.await?;
} else {
error!("Decoding error");
}
}
let db = self.db.clone();
tokio::spawn(async move {
crate::hooks::run_hooks_by_repo_id(
&mut db.get().await.unwrap(),
repo_id,
crate::hooks::Action::APPLY_PATCH,
&hook,
)
.await
});
self.channel = Some(ChannelSpec::Channel(c));
Ok(())
}
async fn apply_to_discussion(
&mut self,
repo: Arc<crate::repository::Repo>,
d: i32,
hash: libpijul::Hash,
channel: thrussh::ChannelId,
session: &mut thrussh::server::Session,
) -> Result<(), crate::Error> {
let id = self.repo_id.clone().unwrap();
let user_id = self.user_id.unwrap();
let user = self.user.clone();
let repo_name = self.repo.clone();
let ip_sql = self.ip_sql.clone();
let config = self.config.clone();
let repo_ = repo.clone();
let (x, err) = self
.db
.get()
.await?
.transaction(|mut txn| {
(async move {
debug!("got transaction");
let mut err = None;
let d = if d == 0 {
use libpijul::changestore::ChangeStore;
let header = if let Ok(h) = repo_.changes.get_header(&hash) {
h
} else {
return Ok((None, None));
};
let (n, _) = if let Some(x) = create_discussion_repo(
&mut txn,
id.repo_id,
user_id,
&header.message,
ip_sql,
)
.await?
{
x
} else {
err = Some(thrussh::CryptoVec::from_slice(
b"A discussion with the same title already exists\n",
));
return Ok((None, err));
};
err = Some(thrussh::CryptoVec::from_slice(
format!(
"Created discussion https://{}/{}/{}/discussions/{}\n",
config.host, user, repo_name, n
)
.as_bytes(),
));
n
} else {
d
};
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
use diesel::sql_types::Bool;
let hashb32 = hash.to_base32();
let n = d::discussions
.filter(d::repository_id.eq(id.repo_id))
.filter(d::number.eq(d))
.filter(
diesel::dsl::sql::<Bool>("not exists(select 1 from discussion_changes join discussions on discussions.id = discussion_changes.discussion where repository_id = ")
.bind::<diesel::sql_types::Uuid, _>(id.repo_id)
.sql(" and number = ")
.bind::<Integer, _>(d)
.sql(" and discussion_changes.change = ")
.bind::<Text, _>(&hashb32)
.sql(" and discussion_changes.removed is null)"),
)
.select((
hashb32.clone().into_sql::<Text>(),
d::id,
user_id.into_sql::<diesel::sql_types::Uuid>().nullable(),
))
.insert_into(dc::discussion_changes)
.into_columns((dc::change, dc::discussion, dc::pushed_by))
.returning(dc::discussion)
.get_result(txn)
.await
.optional()?;
debug!("n = {:?}", n);
if let Some(disc_id) = n {
debug!("{:?} {:?}", id.repo_id, d);
use crate::db::discussion_subscriptions::dsl as ds;
let title = diesel::update(d::discussions.find(disc_id))
.set(d::changes.eq(d::changes + 1))
.returning(d::title)
.get_result(txn)
.await?;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(user_id), ds::discussion_id.eq(disc_id)))
.on_conflict_do_nothing()
.execute(txn)
.await?;
Ok((Some((disc_id, title)), err))
} else {
Err(diesel::result::Error::RollbackTransaction)
}
})
.scope_boxed()
})
.await?;
if let Some((disc_id, title)) = x {
use libpijul::changestore::ChangeStore;
let header = repo.changes.get_header(&hash)?;
let db = self.db.clone();
let repo_id = self.repo_id.as_ref().unwrap();
self.config
.replicator
.send_change(repo_id.origin_id(), hash, false)
.await?;
let hook = pijul_hooks::HookContent::NewChanges {
repository_owner: self.owner.to_string(),
repository_name: self.repo.to_string(),
pusher: self.user.to_string(),
message: header.message.clone(),
author: crate::change::authors_string(&self.db, &header.authors).await?,
hash: hash.to_base32(),
};
let repo_id = repo_id.repo_id;
tokio::spawn(async move {
crate::hooks::run_hooks_by_repo_id(
&mut db.get().await.unwrap(),
repo_id,
crate::hooks::Action::ADD_PATCH,
&hook,
)
.await
});
self.send_change_email(d, disc_id, title, hash, header)
.await?
}
if let Some(err) = err {
session.extended_data(channel, 1, err);
}
self.channel = Some(ChannelSpec::Discussion(d));
Ok(())
}
async fn send_change_email(
&self,
n: i32,
discussion_id: uuid::Uuid,
name: String,
hash: libpijul::pristine::Hash,
header: libpijul::change::ChangeHeader,
) -> Result<(), crate::Error> {
let user_id = if let Some(user) = self.user_id {
user
} else {
return Ok(());
};
use crate::db::discussion_subscriptions::dsl as ds;
use crate::db::users::dsl as u;
let mut db = self.db.get().await?;
let it = ds::discussion_subscriptions
.inner_join(u::users)
.filter(ds::discussion_id.eq(discussion_id))
.filter(u::id.eq(user_id))
.select((u::email, u::login))
.get_results::<(String, String)>(&mut db)
.await?;
let hash = hash.to_base32();
use cuach::Render;
use diesel::sql_types::{Text, Uuid};
let obj = format!("[{}/{}] Changed: {} (#{})", self.owner, self.repo, name, n);
for (address, login) in it {
let n_insert = diesel::sql_query(
"INSERT INTO email_log(discussion, email)
SELECT $1, $2 WHERE (
SELECT COUNT(1) FROM email_log WHERE
discussion = $1
AND email = $2
AND time > NOW() - INTERVAL '1h'
) < 10",
)
.bind::<Uuid, _>(discussion_id)
.bind::<Text, _>(&address)
.execute(&mut db)
.await?;
if n_insert == 0 {
continue;
}
let mut body_html = String::new();
debug!("sending email to {:?}", login);
{
(EmailDiscussionChangeTemplate {
host: &self.config.host,
repo: &self.repo,
owner: &self.owner,
author: &self.user,
login: &login,
disc: n,
name: &name,
hash: &hash,
message: &header.message,
})
.render_into(&mut body_html)
.unwrap();
}
let email_body = rusoto_ses::Body {
text: Some(rusoto_ses::Content {
data: format!(
include_str!("discussions/new_change_email"),
author = self.user,
disc = n,
name = name,
message = header.message,
owner = self.owner,
repo = self.repo,
host = self.config.host,
hash = hash,
login = &login,
),
charset: Some(EMAIL_CHARSET.to_string()),
}),
html: Some(rusoto_ses::Content {
data: body_html,
charset: Some(EMAIL_CHARSET.to_string()),
}),
};
debug!("sending an email to {:?}", address);
let config = self.config.clone();
let obj = obj.clone();
tokio::spawn(async move {
crate::email::send_email(&config, obj, email_body, address).await
});
}
Ok(())
}
}
use crate::email::*;
#[template(path = "discussions/new_change_email.html")]
struct EmailDiscussionChangeTemplate<'a> {
host: &'a str,
owner: &'a str,
repo: &'a str,
author: &'a str,
login: &'a str,
disc: i32,
name: &'a str,
hash: &'a str,
message: &'a str,
}
async fn create_discussion_repo(
client: &mut diesel_async::AsyncPgConnection,
repo: Uuid,
author_id: Uuid,
name: &str,
ip_sql: ipnetwork::IpNetwork,
) -> Result<Option<(i32, Uuid)>, diesel::result::Error> {
use crate::db::discussions::dsl as d;
if d::discussions
.filter(d::repository_id.eq(repo))
.filter(d::closed.is_not_null())
.filter(d::title.eq(&name))
.select(d::id)
.get_result::<uuid::Uuid>(client)
.await
.optional()?
.is_some()
{
return Ok(None);
}
client
.transaction(|mut txn| {
(async move {
use crate::db::discussion_subscriptions::dsl as ds;
use crate::db::repositories::dsl as r;
let number = diesel::update(r::repositories.find(repo))
.filter(crate::has_permissions!(
author_id,
repo,
Perm::CREATE_DISCUSSION.bits()
))
.set(r::next_discussion_number.eq(r::next_discussion_number + 1))
.returning(r::next_discussion_number)
.get_result(&mut txn)
.await
.optional()?;
let number = if let Some(number) = number {
number
} else {
return Err(diesel::result::Error::NotFound);
};
let id = diesel::insert_into(d::discussions)
.values((
d::title.eq(&name),
d::author.eq(&author_id),
d::creation_date.eq(diesel::dsl::now),
d::creation_ip.eq(ip_sql),
d::number.eq(number),
))
.on_conflict_do_nothing()
.returning(d::id)
.get_result::<uuid::Uuid>(&mut txn)
.await?;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(author_id), ds::discussion_id.eq(id)))
.execute(&mut txn)
.await?;
Ok((number, id))
})
.scope_boxed()
})
.await
.optional()
}
use crate::permissions::Perm;
use crate::*;
use crate::{db, get_user_id_strict, get_user_login_strict, Config, Repo, StatusCode};
use axum::{
debug_handler,
extract::{ConnectInfo, Form, State},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use diesel::dsl::sql;
use diesel::sql_types::Bool;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::scoped_futures::ScopedFutureExt;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use thrussh_keys::PublicKeyBase64;
pub fn router() -> Router<Config> {
Router::new()
.route("/", get(settings))
.route("/ssh", get(ssh).post(ssh_post))
.route("/ssh/delete", post(ssh_delete))
.route("/ssh/add", post(ssh_add))
.route("/repo/delete", post(delete_repo))
.route("/repo/add", post(create_repo))
.route("/delete", post(delete))
.fallback(crate::fallback)
}
#[derive(Debug, Serialize)]
struct Settings<T> {
login: String,
token: Option<String>,
#[serde(flatten)]
content: T,
}
#[derive(Debug, Serialize)]
struct UserSettings {
repos: BTreeMap<String, Repo>,
is_owner: bool,
}
#[derive(Debug, Serialize)]
struct Ssh {
ssh_keys: BTreeMap<uuid::Uuid, String>,
}
#[debug_handler]
async fn settings(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
use db::repositories::dsl as r;
let (uid, login) = crate::get_user_login_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let repos = r::repositories
.filter(r::owner.eq(uid))
.order_by(r::name)
.select(r::name)
.get_results::<String>(&mut db)
.await?;
debug!("repos = {:?}", repos);
let resp = Settings {
login,
token: token.authenticity_token().ok(),
content: UserSettings {
repos: repos
.into_iter()
.map(|r| (r.clone(), Repo { private: false }))
.collect(),
is_owner: true,
},
};
debug!("{:?}", resp);
Ok((token, Json(resp)).into_response())
}
#[debug_handler]
async fn ssh(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
use db::publickeys::dsl as publickeys;
use db::users::dsl as u;
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
let user = u::users
.select(u::login)
.filter(u::id.eq(uid))
.filter(u::email_is_invalid.is_null())
.get_result::<String>(&mut db)
.await?;
let ssh_keys = publickeys::publickeys
.filter(publickeys::user_id.eq(uid))
.order_by(publickeys::publickey)
.select((publickeys::id, publickeys::publickey))
.get_results::<(uuid::Uuid, String)>(&mut db)
.await?;
let resp = Settings {
login: user,
token: token.authenticity_token().ok(),
content: Ssh {
ssh_keys: ssh_keys.into_iter().collect(),
},
};
debug!("{:?}", resp);
Ok((token, Json(resp)).into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshDelete {
delete: uuid::Uuid,
token: String,
}
#[debug_handler]
async fn ssh_delete(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
del: Form<SshDelete>,
) -> Result<Response, crate::Error> {
token.verify(&del.token)?;
use db::publickeys::dsl as publickeys;
let uid = get_user_id_strict(&jar)?;
diesel::delete(
publickeys::publickeys
.find(del.delete)
.filter(publickeys::user_id.eq(uid)),
)
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshAdd {
key: String,
token: String,
}
use lazy_static::*;
lazy_static! {
static ref SSH_KEY: regex::Regex =
regex::Regex::new(r#"\s*((ssh-\S+)\s+(?P<key>.*)\s+(\S+))"#).unwrap();
}
#[debug_handler]
async fn ssh_add(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
add: Form<SshAdd>,
) -> Result<Response, crate::Error> {
token.verify(&add.token)?;
let uid = get_user_id_strict(&jar)?;
let bin = if let Some(cap) = SSH_KEY.captures(&add.key) {
let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&add.key);
match thrussh_keys::parse_public_key_base64(b.trim()) {
Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
thrussh_keys::key::PublicKey::RSA {
key,
hash: thrussh_keys::key::SignatureHash::SHA2_512,
}
}
Ok(k) => k,
Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
}
} else {
return Ok(StatusCode::BAD_REQUEST.into_response());
};
use db::publickeys::dsl as publickeys;
diesel::insert_into(publickeys::publickeys)
.values((
publickeys::user_id.eq(uid),
publickeys::publickey.eq(&add.key),
publickeys::bin.eq(&bin.public_key_bytes()),
))
.on_conflict_do_nothing()
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshPost {
id: uuid::Uuid,
key: String,
token: String,
}
#[debug_handler]
async fn ssh_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
post: Form<SshPost>,
) -> Result<Response, crate::Error> {
token.verify(&post.token)?;
let uid = get_user_id_strict(&jar)?;
let bin = if let Some(cap) = SSH_KEY.captures(&post.key) {
let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&post.key);
match thrussh_keys::parse_public_key_base64(b.trim()) {
Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
thrussh_keys::key::PublicKey::RSA {
key,
hash: thrussh_keys::key::SignatureHash::SHA2_512,
}
}
Ok(k) => k,
Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
}
} else {
return Ok(StatusCode::BAD_REQUEST.into_response());
};
use db::publickeys::dsl as publickeys;
diesel::update(publickeys::publickeys.find(post.id))
.filter(publickeys::user_id.eq(uid))
.set((
publickeys::publickey.eq(&post.key),
publickeys::bin.eq(&bin.public_key_bytes()),
))
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateRepo {
name: String,
#[serde(default)]
private: bool,
token: String,
}
#[debug_handler]
async fn create_repo(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(create): Form<CreateRepo>,
) -> Result<Response, crate::Error> {
token.verify(&create.token)?;
use db::permissions::dsl as p;
use db::repositories::dsl as r;
use db::users::dsl as u;
debug!("create repo {:?}", create);
let mut db_ = config.db.get().await?;
let owner = get_user_id_strict(&jar)?;
db_.transaction(|mut txn| {
(async move {
let login = if let Some(login) = u::users
.find(owner)
.select(u::login)
.get_result::<String>(&mut txn)
.await
.optional()?
{
login
} else {
return Ok(StatusCode::FORBIDDEN.into_response());
};
let repo_id = diesel::insert_into(r::repositories)
.values((
r::owner.eq(owner),
r::name.eq(create.name),
r::creation_ip.eq(addr.to_string()),
))
.on_conflict_do_nothing()
.returning(r::id)
.get_result::<uuid::Uuid>(&mut txn)
.await?;
diesel::insert_into(p::permissions)
.values((
p::user_id.eq(owner),
p::repo_id.eq(repo_id),
p::start_date.eq(diesel::dsl::now),
p::perm.eq(Perm::create_owner().bits()),
))
.execute(&mut txn)
.await?;
if !create.private {
diesel::insert_into(p::permissions)
.values((
p::user_id.eq(uuid::Uuid::nil()),
p::repo_id.eq(repo_id),
p::start_date.eq(diesel::dsl::now),
p::perm.eq(Perm::create_public().bits()),
))
.execute(&mut txn)
.await?;
}
Ok(Redirect::to(&format!("/{}", login)).into_response())
})
.scope_boxed()
})
.await
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteRepo {
name: String,
token: String,
}
#[debug_handler]
async fn delete_repo(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Form(delete): Form<DeleteRepo>,
) -> Result<Response, crate::Error> {
token.verify(&delete.token)?;
let (owner, login) = get_user_login_strict(&jar, &config).await?;
use db::repositories::dsl as r;
if let Some(id) = diesel::delete(r::repositories)
.filter(r::owner.eq(owner))
.filter(r::name.eq(delete.name))
.returning(r::id)
.get_result(&mut config.db.get().await?)
.await.optional()?
{
config.repo_locks.remove(&id).await?;
}
Ok(Redirect::to(&format!("/{}", login)).into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteAccount {
token: String,
password: String,
}
#[debug_handler]
async fn delete(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Form(delete): Form<DeleteAccount>,
) -> Result<Response, crate::Error> {
use db::users::dsl as u;
token.verify(&delete.token)?;
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
let n = diesel::delete(u::users.find(uid).filter(check_password!(&delete.password)))
.execute(&mut db)
.await
.unwrap();
if n == 0 {
use crate::db::profile_pics::dsl as pp;
use crate::db::publickeys::dsl as pk;
use crate::db::signingkeys::dsl as sk;
// We couldn't delete because of conflicts.
diesel::update(u::users.find(uid).filter(check_password!(&delete.password)))
.set(u::login.eq(sql("md5(concat(users.id::text, users.login))")))
.execute(&mut db)
.await?;
diesel::delete(pp::profile_pics.find(uid)).execute(&mut db).await?;
diesel::delete(pk::publickeys.filter(pk::user_id.eq(uid))).execute(&mut db).await?;
diesel::delete(sk::signingkeys.filter(sk::user_id.eq(uid))).execute(&mut db).await?;
}
Ok(Redirect::to("/").into_response())
}
use crate::permissions::Perm;
use crate::Config;
use axum::{
debug_handler,
extract::{Query, State},
response::{IntoResponse, Response},
Json,
};
use axum_extra::extract::SignedCookieJar;
use libpijul::{
pristine::{ChangeId, Position, Vertex},
vertex_buffer::VertexBuffer,
Base32, ChannelTxnT, TxnT, TxnTExt,
};
use serde_derive::*;
use tracing::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct PosQuery {
channel: Option<String>,
pos: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Tree {
owner: String,
repo: String,
channel: String,
channels: Vec<String>,
can_delete: bool,
inode: Inode,
#[serde(skip_serializing_if = "Option::is_none")]
hled: Option<String>,
token: String,
login: Option<String>,
}
#[derive(Debug, Serialize)]
pub enum Inode {
File {
path: Vec<super::PathElement>,
file: Option<String>,
},
Directory {
path: Vec<super::PathElement>,
children: Vec<Directory>,
},
}
#[derive(Debug, Serialize)]
pub struct Directory {
is_dir: bool,
meta: libpijul::pristine::InodeMetadata,
name: String,
pos: String,
}
#[debug_handler]
pub async fn tree(
State(config): State<Config>,
jar: SignedCookieJar,
tree: crate::repository::RepoPath,
token: axum_csrf::CsrfToken,
pos: Query<PosQuery>,
) -> Result<Response, crate::Error> {
debug!("tree {:?}", tree);
let (uid, login) = crate::get_user_login_(&jar, &config).await?;
let mut db = config.db.get().await?;
let (id, _) = super::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
let c = super::channel_spec_id(id, tree.channel.as_deref().unwrap_or("main"));
debug!("channel {:?}", c);
let locks = config.repo_locks.clone();
let repo_ = locks.get(&id).await.unwrap();
let channels = repo_.channels().await;
let channel = tree.channel.unwrap_or_else(|| "main".to_string());
let channel_ = channel.clone();
let inode = tokio::task::spawn_blocking(move || {
let pristine = repo_.pristine.blocking_read();
let txn = pristine.txn_begin()?;
let channel = if let Some(channel) = txn.load_channel(&c)? {
channel
} else if txn.channels("")?.is_empty() && (pos.channel.as_deref() == Some("main") || pos.channel.is_none()) {
return Ok(Inode::Directory {
path: Vec::new(),
children: Vec::new(),
});
} else {
debug!("channel not found {:?}", c);
if tracing::enabled!(tracing::Level::DEBUG) {
for c in txn.channels("")? {
let c = c.read();
debug!("channel: {:?}", c.name);
}
}
return Err(crate::Error::ChannelNotFound { channel: channel_ });
};
let ch = channel.read();
let pos = pos
.pos
.as_ref()
.map(String::as_bytes)
.and_then(Position::from_base32);
let pos = pos.unwrap_or(Position::ROOT);
if !txn.is_alive(&*ch, &pos.inode_vertex())? {
debug!("not alive {:?}", pos.inode_vertex());
return Err(crate::Error::InodeNotFound);
}
let current_path = super::current_path(&txn, &*ch, &repo_.changes, pos)?;
debug!("current_path = {:?}", current_path);
let is_dir = if let Some(path) = current_path.last() {
debug!("last path = {:?}", path);
path.meta.is_dir()
} else {
true
};
if is_dir {
let mut children = Vec::new();
for x in libpijul::fs::iter_graph_children(&txn, &repo_.changes, txn.graph(&*ch), pos)?
{
let (pos, _, meta, name) = x?;
children.push(Directory {
is_dir: meta.is_dir(),
meta,
pos: pos.to_base32(),
name,
})
}
children.sort_by(|a, b| (!a.is_dir, &a.name).cmp(&(!b.is_dir, &b.name)));
Ok(Inode::Directory {
path: current_path,
children,
})
} else {
let mut out = RawVertexBuf { out: Vec::new() };
let txn = libpijul::ArcTxn::new(txn);
libpijul::output::output_file(&repo_.changes, &txn, &channel, pos, &mut out)
.map_err(|_| crate::Error::Txn)?;
Ok(Inode::File {
path: current_path,
file: String::from_utf8(out.out).ok(),
})
}
})
.await??;
let t = Tree {
owner: tree.owner,
repo: tree.repo,
channel,
channels,
can_delete: true,
inode,
hled: None,
token: token.authenticity_token()?,
login,
};
debug!("{:?}", t);
Ok((token, Json(t)).into_response())
}
#[derive(Debug)]
struct RawVertexBuf {
out: Vec<u8>,
}
impl VertexBuffer for RawVertexBuf {
fn output_line<E, C: FnOnce(&mut [u8]) -> Result<(), E>>(
&mut self,
v: Vertex<ChangeId>,
contents: C,
) -> Result<(), E> {
let len = self.out.len();
self.out.resize(len + (v.end - v.start), 0);
contents(&mut self.out[len..])?;
Ok(())
}
fn output_conflict_marker<T>(
&mut self,
marker: &str,
_: usize,
_: Option<(&T, &[&libpijul::Hash])>,
) -> Result<(), std::io::Error> {
self.out.extend(marker.as_bytes());
Ok(())
}
}
use crate::config::Config;
use crate::permissions::*;
use diesel::sql_types::Bool;
use diesel::{
ExpressionMethods, OptionalExtension, QueryDsl,
};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use libpijul::{Base32, MutTxnT};
use lru_cache::LruCache;
use serde_derive::*;
use std::fs::{create_dir_all, metadata};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use tracing::*;
use uuid::Uuid;
pub mod changestore;
pub mod router;
pub mod admin;
#[derive(Debug, Deserialize)]
pub struct TreePath {
pub owner: String,
pub repo: String,
}
#[derive(Serialize)]
pub struct RepositoryPath<'a> {
pub owner: &'a str,
pub repo: &'a str,
pub path: &'a [PathElement],
pub n_followers: u64,
pub user_is_follower: bool,
}
#[derive(Serialize)]
pub struct Description<'a> {
pub descr: &'a str,
pub repo_id: uuid::Uuid,
}
#[derive(Clone, Debug)]
pub struct RepositoryId {
pub owner_id: Uuid,
pub repo_id: Uuid,
pub fork_origin: Option<Uuid>,
}
pub fn nest_path(config: &Config, fork_origin: Uuid) -> PathBuf {
let mut p = config.repositories_path.clone();
p.push(&format!("{}", fork_origin));
p
}
pub fn nest_pristine_path(config: &Config, fork_origin: Uuid) -> PathBuf {
let mut p = nest_path(config, fork_origin);
p.push(".pijul");
p.push("pristine");
p
}
pub fn nest_changes_path(config: &Config, fork_origin: Uuid) -> PathBuf {
let mut p = nest_path(config, fork_origin);
p.push(".pijul");
p.push("changes");
p
}
impl RepositoryId {
pub fn origin_id(&self) -> Uuid {
self.fork_origin.unwrap_or(self.repo_id)
}
pub fn nest_changes_path(&self, config: &Config) -> PathBuf {
nest_changes_path(config, self.origin_id())
}
}
type Locks = Mutex<LruCache<Uuid, Arc<Repo>>>;
pub struct Repo {
pub pristine: RwLock<libpijul::pristine::sanakirja::Pristine>,
pub changes: changestore::FileSystem,
}
#[derive(Clone)]
pub struct RepositoryLocks {
pub config: Arc<Config>,
locks: Pin<Arc<Locks>>,
}
impl std::fmt::Debug for RepositoryLocks {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(fmt, "RepositoryLocks {{ … }}")
}
}
impl RepositoryLocks {
pub fn new(config: Arc<Config>) -> Self {
let cache_size = config.repository_cache_size;
RepositoryLocks {
config,
locks: Arc::pin(Mutex::new(LruCache::new(cache_size))),
}
}
pub async fn remove(&self, repo_id: &uuid::Uuid) -> Result<(), crate::Error> {
let mut locks = self.locks.lock().await;
drop(locks.remove(repo_id));
Ok(())
}
pub async fn get(&self, repo_id: &Uuid) -> Result<Arc<Repo>, crate::Error> {
debug!("get: taking lock {:?}", repo_id);
let mut locks = self.locks.lock().await;
unsafe {
info!(
"repositorylocks size: {:?} {:?}",
locks.len(),
heapsize::heap_size_of::<LruCache<_, _>>(&*locks)
);
}
debug!("get: lock taken");
{
if let Some(repo_lock) = locks.get_mut(repo_id).map(|x| x.clone()) {
debug!("get: found");
std::mem::drop(locks);
return Ok(repo_lock);
}
}
debug!("get: not found, inserting");
let path = nest_pristine_path(&self.config, *repo_id);
debug!("get: open_or_create {:?}", path);
let repo = unsafe {
// Ok to do here because we have a lock.
self.open_or_create_repository(&path)?
};
debug!("get: open_or_create done");
let path = nest_changes_path(&self.config, *repo_id);
debug!("path = {:?}", path);
create_dir_all(&path)?;
let repo = Arc::new(Repo {
pristine: RwLock::new(repo),
changes: changestore::FileSystem {
change_cache: self.config.change_cache.clone(),
hash_cache: self.config.hash_cache.clone(),
changes_dir: path,
id: *repo_id,
db: self.config.db.clone(),
},
});
locks.insert(*repo_id, repo.clone());
std::mem::drop(locks);
debug!("get: done");
Ok(repo)
}
// `branch` comes with its prefix
unsafe fn open_or_create_repository(
&self,
path: &Path,
) -> Result<libpijul::pristine::sanakirja::Pristine, crate::Error> {
if metadata(&path).is_err() {
create_dir_all(&path)?;
let repo = libpijul::pristine::sanakirja::Pristine::new_nolock(&path.join("db"))?;
repo.mut_txn_begin()?.commit()?;
Ok(repo)
} else {
Ok(libpijul::pristine::sanakirja::Pristine::new_nolock(
&path.join("db"),
)?)
}
}
}
#[derive(Debug)]
pub enum ChannelSpec {
Channel(String),
CI(String),
Discussion(i32),
}
impl ChannelSpec {
pub fn channel(c: std::borrow::Cow<str>) -> Self {
if c.ends_with("/ci") {
ChannelSpec::CI(c.split_at(c.len() - 3).0.to_string())
} else {
ChannelSpec::Channel(c.into_owned())
}
}
}
pub fn channel_spec(repo_id: &RepositoryId, channel: &str) -> String {
if channel.ends_with("/ci") {
format!(
"{}_{}",
repo_id.repo_id,
channel.split_at(channel.len() - 3).0
)
} else {
format!("{}_{}", repo_id.repo_id, channel)
}
}
pub fn channel_spec_id(repo_id: uuid::Uuid, channel: &str) -> String {
if channel.ends_with("/ci") {
format!("{}_{}", repo_id, channel.split_at(channel.len() - 3).0)
} else {
format!("{}_{}", repo_id, channel)
}
}
pub async fn repository_id(
db: &mut AsyncPgConnection,
owner: &str,
name: &str,
user: Option<uuid::Uuid>,
required: Perm,
) -> Result<(Uuid, Perm), crate::Error> {
use crate::db::repositories::dsl as r;
use crate::db::users::dsl as u;
if let Some((rid, perms, suspended, is_public)) = r::repositories
.inner_join(u::users)
.filter(u::login.eq(owner))
.filter(u::is_active)
.filter(r::name.eq(name))
.filter(r::is_active)
.select((
r::id,
crate::permissions!(user.unwrap_or(uuid::Uuid::nil()), r::id),
u::suspended,
is_public(),
))
.get_result::<(uuid::Uuid, i64, bool, bool)>(db)
.await.optional()?
{
let got = if suspended {
Perm::empty()
} else if is_public {
Perm::from_bits(perms).unwrap() | Perm::READ
} else {
Perm::from_bits(perms).unwrap()
};
if got.contains(required) {
Ok((rid, got))
} else {
Err(crate::Error::Permissions {
required, got
})
}
} else {
Err(crate::Error::RepositoryNotFound)
}
}
pub fn is_public() -> diesel::expression::SqlLiteral<Bool> {
diesel::dsl::sql::<Bool>("EXISTS (SELECT 1 FROM permissions WHERE permissions.user_id = '00000000-0000-0000-0000-000000000000' AND permissions.repo_id = repositories.id)")
}
pub async fn repository(
db: &mut AsyncPgConnection,
owner: &str,
name: &str,
user: Uuid,
required: Perm,
) -> Result<Repository, crate::Error> {
use crate::db::repositories::dsl as r;
use crate::db::users::dsl as u;
if let Some((
repo_id,
perms,
suspended,
is_public,
)) = r::repositories
.inner_join(u::users)
.filter(u::login.eq(owner))
.filter(u::is_active)
.filter(r::name.eq(name))
.filter(r::is_active)
.select((
r::id,
crate::permissions!(user, r::id),
u::suspended,
is_public(),
))
.get_result::<(
uuid::Uuid,
i64,
bool,
bool,
)>(db)
.await
.optional()?
{
let got = if suspended {
Perm::empty()
} else if is_public {
Perm::from_bits(perms).unwrap() | Perm::READ
} else {
Perm::from_bits(perms).unwrap()
};
if !got.contains(required) {
Err(crate::Error::Permissions { required, got })
} else {
Ok(Repository {
id: repo_id,
permissions: got,
})
}
} else {
Err(crate::Error::RepositoryNotFound)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RepoPath {
pub owner: String,
pub repo: String,
pub channel: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct RepoPath_ {
owner: String,
repo: String,
}
impl<S> axum::extract::FromRequestParts<S> for RepoPath
where
S: Send + Sync,
{
type Rejection =
<axum::extract::Path<RepoPath_> as axum::extract::FromRequestParts<S>>::Rejection;
async fn from_request_parts(
parts: &mut http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
use axum::extract::Path;
let Path(r) = Path::<RepoPath_>::from_request_parts(parts, state).await?;
debug!("path r {:?}", r);
let mut it = r.repo.split(':');
Ok(RepoPath {
owner: r.owner,
repo: it.next().unwrap().to_string(),
channel: it.next().map(|x| x.to_string()),
})
}
}
impl Repo {
pub async fn channels(&self) -> Vec<String> {
use libpijul::TxnT;
let mut c = Vec::new();
let pristine = self.pristine.read().await;
let txn = pristine.txn_begin().unwrap();
let mut at_least_one = false;
for chan in txn.channels("").unwrap() {
at_least_one = true;
let name = {
let s = chan.read();
debug!("channels: {:?}", s.name.as_str());
let (_, s) = s.name.as_str().split_at(37);
s.to_string()
};
c.push(name)
}
c.sort();
if !at_least_one {
c.push("main".to_string())
}
c
}
}
pub async fn free_used_storage(
db: &mut diesel_async::AsyncPgConnection,
repo: uuid::Uuid,
size: u64,
) -> Result<(), diesel::result::Error> {
use crate::db::repositories::dsl as repositories;
use crate::db::users::dsl as users;
use diesel::sql_types::BigInt;
let n = diesel::update(users::users)
.filter(
users::id.eq_any(
repositories::repositories
.filter(repositories::id.eq(repo))
.select(repositories::owner),
),
)
.set(
users::storage_used.eq(diesel::dsl::sql("GREATEST(0, storage_used - ")
.bind::<BigInt, _>(size as i64)
.sql(")")),
)
.execute(db)
.await?;
debug!("del_change updated {:?}", n);
Ok(())
}
#[derive(Debug)]
pub struct Repository {
pub id: uuid::Uuid,
pub permissions: Perm,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Identity {
pub public_key: libpijul::key::PublicKey,
pub login: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub origin: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
pub last_modified: u64,
}
#[derive(Debug, Serialize)]
pub struct PathElement {
pub basename: String,
pub pos: String,
pub meta: libpijul::pristine::InodeMetadata,
}
fn current_path<
T: libpijul::GraphTxnT + libpijul::ChannelTxnT,
C: libpijul::changestore::ChangeStore,
>(
txn: &T,
channel: &T::Channel,
changes: &C,
pos: libpijul::pristine::Position<libpijul::ChangeId>,
) -> Result<Vec<PathElement>, crate::Error> {
let mut current_path = Vec::new();
// Deciding whether this is a file or a directory.
{
let mut pos = pos;
while !pos.is_root() {
if let Some(x) = libpijul::fs::iter_basenames(txn, changes, txn.graph(channel), pos)
.map_err(|_| crate::Error::Txn)?
.next()
{
let (grandparent, meta, basename) = x.map_err(|_| crate::Error::Txn)?;
current_path.push(PathElement {
basename: basename,
pos: pos.to_base32(),
meta,
});
pos = grandparent
} else {
// non-null root.
break;
}
}
}
current_path.reverse();
Ok(current_path)
}
use libpijul::change::{Change, ChangeFile, ChangeHeader};
use libpijul::changestore::filesystem::*;
use libpijul::changestore::*;
use libpijul::pristine::{Base32, ChangeId, Hash, Merkle, Position, Vertex};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Mutex, MutexGuard};
use thiserror::*;
use tracing::*;
// use crate::config::Db;
#[derive(Clone)]
pub struct FileSystem {
pub change_cache:
Arc<Mutex<lru_cache::LruCache<(uuid::Uuid, ChangeId), Arc<Mutex<ChangeFile>>>>>,
pub hash_cache: Arc<Mutex<lru_cache::LruCache<(uuid::Uuid, Hash), Arc<Mutex<ChangeFile>>>>>,
pub id: uuid::Uuid,
pub changes_dir: PathBuf,
pub db: crate::config::Db,
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Tag(#[from] libpijul::tag::TagError),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
ChangeFile(#[from] libpijul::change::ChangeError),
#[error(transparent)]
Persist(#[from] tempfile::PersistError),
#[error("Change too large")]
TooLarge,
}
pub fn push_filename(changes_dir: &mut PathBuf, hash: &Hash) {
let h32 = hash.to_base32();
let (a, b) = h32.split_at(2);
changes_dir.push(a);
changes_dir.push(b);
changes_dir.set_extension("change");
}
impl FileSystem {
pub fn filename(&self, hash: &Hash) -> PathBuf {
let mut path = self.changes_dir.clone();
push_filename(&mut path, hash);
path
}
pub fn tag_filename(&self, hash: &Merkle) -> PathBuf {
let mut path = self.changes_dir.clone();
push_tag_filename(&mut path, hash);
path
}
pub fn has_change(&self, hash: &Hash) -> bool {
std::fs::metadata(&self.filename(hash)).is_ok()
}
fn load<'a, F: Fn(ChangeId) -> Option<Hash>>(
&'a self,
hash: F,
change: ChangeId,
) -> Result<
MutexGuard<'a, lru_cache::LruCache<(uuid::Uuid, ChangeId), Arc<Mutex<ChangeFile>>>>,
libpijul::change::ChangeError,
> {
let mut cache = self.change_cache.lock().unwrap();
debug!("cache size = {:?}", cache.len());
if !cache.contains_key(&(self.id, change)) {
let h = hash(change).unwrap();
let mut cache_ = self.hash_cache.lock().unwrap();
if let Some(p) = cache_.get_mut(&(self.id, h)) {
cache.insert((self.id, change), p.clone());
} else {
debug!("cache does not contain {:?} {:?}", change, h);
let path = self.filename(&h);
let p = Arc::new(Mutex::new(libpijul::change::ChangeFile::open(
h,
&path.to_str().unwrap(),
)?));
cache.insert((self.id, change), p.clone());
cache_.insert((self.id, h), p);
}
}
Ok(cache)
}
pub async fn check(
&self,
file: &mut tokio::fs::File,
hash: &Hash,
change_id: Option<ChangeId>,
) -> Result<(), libpijul::change::ChangeError> {
check_from_reader(file, hash).await?;
if let Some(change_id) = change_id {
let mut cache = self.change_cache.lock().unwrap();
cache.remove(&(self.id, change_id));
}
Ok(())
}
}
use libpijul::change::*;
use tokio::io::SeekFrom;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
pub async fn check_from_reader<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
mut r: R,
hash: &Hash,
) -> Result<Hashed<Hunk<Option<Hash>, Local>, Author>, libpijul::change::ChangeError> {
use libpijul::change::*;
use libpijul::pristine::Hasher;
let mut buf = [0; 4096];
debug!("read");
r.read_exact(&mut buf[..Change::OFFSETS_SIZE as usize])
.await?;
debug!("read done");
let offsets: Offsets = bincode::deserialize(&buf)?;
if offsets.version != VERSION && offsets.version != VERSION_NOENC {
return Err(ChangeError::VersionMismatch {
got: offsets.version,
});
}
// Hashing the hashed part
let mut hasher = Hasher::default();
let mut dstream = zstd_seekable::DStream::new()?;
let mut n = 0;
let mut input = [0; 4096];
let len = (offsets.unhashed_off - Change::OFFSETS_SIZE) as usize;
let mut out: Vec<u8> = Vec::new();
while n < len {
let len = (len - n).min(4096);
let mut ia = 0;
let ib = r.read(&mut input[..len]).await? as usize;
debug!("read {:?} bytes", ib);
if ib == 0 {
break;
}
while ia < ib {
let (a, b) = dstream.decompress(&mut buf[..], &input[ia..ib])?;
debug!("decompressed {:?} bytes from compressed {:?}", a, b);
hasher.update(&buf[..a]);
out.extend(&buf[..a]);
ia += b;
}
n += ib;
}
let computed_hash = hasher.finish();
debug!("{:?} {:?}", computed_hash, hash);
if &computed_hash != hash {
return Err((ChangeError::ChangeHashMismatch {
claimed: *hash,
computed: computed_hash,
})
.into());
}
let hashed: Hashed<Hunk<Option<Hash>, Local>, Author> = if offsets.version == VERSION {
bincode::deserialize(&out)?
} else {
let h: Hashed<noenc::Hunk<Option<Hash>, Local>, noenc::Author> =
bincode::deserialize(&out)?;
h.into()
};
let mut hasher = Hasher::default();
r.seek(SeekFrom::Start(offsets.contents_off)).await?;
let mut dstream = zstd_seekable::DStream::new()?;
let mut n = 0;
let mut input = [0; 4096];
let len = offsets.total - offsets.contents_off;
while n < len {
let len = (len - n).min(4096);
let mut ia = 0;
let ib = r.read(&mut input[..len as usize]).await?;
debug!("read {:?} bytes", ib);
if ib == 0 {
break;
}
while ia < ib {
let (a, b) = dstream.decompress(&mut buf[..], &input[ia..ib])?;
debug!("decompressed {:?} bytes from compressed {:?}", a, b);
hasher.update(&buf[..a]);
ia += b;
}
n += ib as u64;
}
let computed_hash = hasher.finish();
debug!(
"contents hash: {:?}, computed: {:?}",
hashed.contents_hash, computed_hash
);
if computed_hash != hashed.contents_hash {
return Err(ChangeError::ContentsHashMismatch {
claimed: hashed.contents_hash,
computed: computed_hash,
});
}
Ok(hashed)
}
impl ChangeStore for FileSystem {
type Error = Error;
fn has_contents(&self, hash: Hash, change_id: Option<ChangeId>) -> bool {
if let Some(change_id) = change_id {
let mut cache = self.load(|_| Some(hash), change_id).unwrap();
let mut poisoned = false;
if let Some(c) = cache.get_mut(&(self.id, change_id)) {
if let Ok(l) = c.lock() {
return l.has_contents();
} else {
poisoned = true
}
}
if poisoned {
cache.remove(&(self.id, change_id));
}
}
debug!("has_contents {:?} {:?}", hash, change_id);
let path = self.filename(&hash);
if let Ok(p) = libpijul::change::ChangeFile::open(hash, &path.to_str().unwrap()) {
p.has_contents()
} else {
false
}
}
fn get_header(&self, h: &Hash) -> Result<ChangeHeader, Self::Error> {
debug!("get_header {:?}", h);
let mut cache = self.hash_cache.lock().unwrap();
if let Some(c) = cache.get_mut(&(self.id, *h)) {
return Ok(c.lock().unwrap().hashed().header.clone());
}
let path = self.filename(h);
let p = libpijul::change::ChangeFile::open(*h, &path.to_str().unwrap())?;
let hdr = p.hashed().header.clone();
cache.insert((self.id, *h), Arc::new(Mutex::new(p)));
Ok(hdr)
}
fn get_tag_header(&self, h: &Merkle) -> Result<ChangeHeader, Self::Error> {
let path = self.tag_filename(h);
let mut p = libpijul::tag::OpenTagFile::open(&path, h)?;
Ok(p.header()?)
}
fn get_contents<F: Fn(ChangeId) -> Option<Hash>>(
&self,
hash: F,
key: Vertex<ChangeId>,
buf: &mut [u8],
) -> Result<usize, Self::Error> {
let key_end: u64 = key.end.0.into();
let key_start: u64 = key.start.0.into();
// buf.resize((key_end - key_start) as usize, 0);
if key_end <= key_start || key.is_root() {
return Ok(0);
}
let mut cache = self.load(hash, key.change)?;
let p = cache.get_mut(&(self.id, key.change)).unwrap();
let mut p = p.lock().unwrap();
let n = p.read_contents(key.start.0.into(), buf)?;
Ok(n)
}
fn get_contents_ext(
&self,
key: Vertex<Option<Hash>>,
buf: &mut [u8],
) -> Result<usize, Self::Error> {
debug!("get_contents_ext {:?}", key);
if let Some(change) = key.change {
// let key_end: u64 = key.end.0.into();
// let key_start: u64 = key.start.0.into();
// buf.resize((key_end - key_start) as usize, 0);
if key.end <= key.start {
return Ok(0);
}
{
if let Some(p) = self.hash_cache.lock().unwrap().get_mut(&(self.id, change)) {
let n = p.lock().unwrap().read_contents(key.start.0.into(), buf)?;
return Ok(n);
}
}
let path = self.filename(&change);
let mut p = libpijul::change::ChangeFile::open(change, &path.to_str().unwrap())?;
let n = p.read_contents(key.start.0.into(), buf)?;
debug!("taking lock");
self.hash_cache
.lock()
.unwrap()
.insert((self.id, change), Arc::new(Mutex::new(p)));
debug!("inserted");
Ok(n)
} else {
Ok(0)
}
}
fn change_deletes_position<F: Fn(ChangeId) -> Option<Hash>>(
&self,
hash: F,
change: ChangeId,
pos: Position<Option<Hash>>,
) -> Result<Vec<Hash>, Self::Error> {
let mut cache = self.load(hash, change)?;
let p = cache.get_mut(&(self.id, change)).unwrap();
let p = p.lock().unwrap();
let mut v = Vec::new();
for c in p.hashed().changes.iter() {
for c in c.iter() {
v.extend(c.deletes_pos(pos).into_iter())
}
}
Ok(v)
}
fn save_change<
E: From<Self::Error> + From<libpijul::change::ChangeError>,
F: FnOnce(&mut Change, &Hash) -> Result<(), E>,
>(
&self,
p: &mut Change,
ff: F,
) -> Result<Hash, E> {
let mut f = match tempfile::NamedTempFile::new_in(&self.changes_dir) {
Ok(f) => f,
Err(e) => return Err(E::from(Self::Error::from(e))),
};
let hash = {
let w = std::io::BufWriter::new(&mut f);
p.serialize(w, ff)?
};
let file_name = self.filename(&hash);
if let Err(e) = std::fs::create_dir_all(file_name.parent().unwrap()) {
return Err(E::from(Self::Error::from(e)));
}
debug!("file_name = {:?}", file_name);
if let Err(e) = f.persist(file_name) {
return Err(E::from(Self::Error::from(e)));
}
Ok(hash)
}
fn del_change(&self, hash: &Hash) -> Result<bool, Self::Error> {
let file_name = self.filename(hash);
debug!("file_name = {:?}", file_name);
let db = self.db.clone();
let id = self.id.clone();
tokio::spawn(async move {
if let Ok(meta) = tokio::fs::metadata(&file_name).await {
if tokio::fs::remove_file(&file_name).await.is_ok() {
tokio::fs::remove_dir(file_name.parent().unwrap())
.await
.unwrap_or(()); // fails silently if there are still changes with the same 2-letter prefix.
super::free_used_storage(&mut *db.get().await?, id, meta.len()).await?
/*
let n = db.execute(
"UPDATE users SET storage_used = GREATEST(0, storage_used - $2)
FROM repositories
WHERE repositories.id = $1 AND repositories.owner = users.id",
&[&id, &(meta.len() as i64)]
).await?;
*/
}
}
Ok::<(), crate::Error>(())
});
Ok(true)
}
fn get_change(&self, h: &Hash) -> Result<Change, Self::Error> {
let file_name = self.filename(h);
let file_name = file_name.to_str().unwrap();
debug!("file_name = {:?}", file_name);
let m = std::fs::metadata(&file_name)?;
if m.len() >= MAX_CHANGE_SIZE {
info!("Change {:?} {:?} too large", self.id, h);
// return Err(Error::TooLarge)
}
Ok(Change::deserialize(&file_name, Some(h))?)
}
}
// 4Mb
const MAX_CHANGE_SIZE: u64 = 1 << 22;
use crate::permissions::Perm;
use crate::repository::TreePath;
use crate::{config::AcceptJson, get_user_id_strict, Config, Redirect};
use axum::{
debug_handler,
extract::{Form, Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use axum_extra::TypedHeader;
use diesel::dsl::now;
use diesel::expression::AsExpression;
use diesel::upsert::excluded;
use diesel::{BoolExpressionMethods, ExpressionMethods, Insertable, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use serde_derive::*;
use tracing::*;
pub fn router() -> Router<Config> {
Router::new()
.route("/", get(admin))
.route("/permission", post(perm))
.route("/tag", post(tag))
.fallback(crate::fallback)
}
#[derive(Debug, Serialize)]
struct Admin {
token: Option<String>,
owner: String,
repo: String,
is_private: bool,
permissions: Vec<LoginPerm>,
tags: Vec<Tag>,
login: String,
}
#[derive(Debug, Serialize)]
struct LoginPerm {
login: String,
perm: i64,
}
#[derive(Debug, Serialize)]
struct Tag {
name: String,
color: i32,
id: uuid::Uuid,
}
#[debug_handler]
pub async fn admin(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
tree: crate::repository::RepoPath,
) -> Result<Response, crate::Error> {
let (uid, login) = crate::get_user_login_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let id = if let Some(id) = r::repositories
.inner_join(u::users)
.filter(u::login.eq(&tree.owner))
.filter(r::name.eq(&tree.repo))
.filter(u::id.eq(uid))
.select(r::id)
.get_result::<uuid::Uuid>(&mut db)
.await
.optional()?
{
id
} else {
return Ok((StatusCode::FORBIDDEN, "{}").into_response());
};
use crate::db::permissions::dsl as p;
use crate::db::repositories::dsl as r;
use crate::db::tags::dsl as t;
use crate::db::users::dsl as u;
let mut is_private = true;
let permissions = p::permissions
.inner_join(u::users)
.inner_join(r::repositories)
.filter(p::repo_id.eq(id))
.filter(p::end_date.is_null().or(p::end_date.gt(now)))
.select((u::id, u::login, p::perm))
.order_by(p::start_date.desc())
.get_results::<(uuid::Uuid, String, i64)>(&mut db)
.await?
.into_iter()
.filter_map(|(id, login, perm)| {
debug!("{:?} {:?} {:?}", id, login, perm);
if id.is_nil() {
let perm = Perm::from_bits(perm).unwrap();
is_private = !perm.contains(Perm::READ);
None
} else {
Some(LoginPerm { login, perm })
}
})
.collect();
let tags = t::tags
.filter(t::repository_id.eq(id))
.select((t::id, t::name, t::color))
.get_results::<(uuid::Uuid, String, i32)>(&mut db)
.await?
.into_iter()
.map(|(id, name, color)| Tag { id, name, color })
.collect();
let token_ = token.authenticity_token().ok();
Ok((
token,
Json(Admin {
token: token_,
owner: tree.owner,
repo: tree.repo,
is_private,
permissions,
tags,
login,
}),
)
.into_response())
}
#[derive(Debug, Deserialize)]
struct PermForm {
token: String,
#[serde(default)]
login: Option<String>,
}
#[debug_handler]
async fn perm(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<TreePath>,
body: bytes::Bytes,
) -> Result<Response, crate::Error> {
let form: PermForm = serde_urlencoded::from_bytes(&body)?;
token.verify(&form.token)?;
let mut perm = Perm::empty();
for (k, v) in url::form_urlencoded::parse(&body) {
if k.starts_with("p") && v == "on" {
let (_, b) = k.split_at(1);
if let Some(b) = b.parse().ok().and_then(|x: i32| Perm::from_bits(1 << x)) {
perm |= b
}
}
}
debug!("new {:?}", perm);
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
use crate::db::permissions::dsl as p;
use crate::db::users::dsl as u;
use diesel::sql_types::{BigInt, Uuid};
let (id, _) = crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
Some(uid),
Perm::EDIT_PERMISSIONS,
)
.await?;
if let Some(l) = form.login {
if l != tree.owner {
u::users
.filter(u::login.eq(&l))
.select((
u::id,
&AsExpression::<Uuid>::as_expression(id),
&AsExpression::<BigInt>::as_expression(perm.bits()),
))
.insert_into(p::permissions)
.into_columns((p::user_id, p::repo_id, p::perm))
.on_conflict((p::user_id, p::repo_id))
.do_update()
.set(p::user_id.eq(excluded(p::user_id)))
.execute(&mut db)
.await?;
}
} else {
diesel::insert_into(p::permissions)
.values((
p::user_id.eq(uuid::Uuid::nil()),
p::repo_id.eq(id),
p::perm.eq(perm.bits()),
))
.on_conflict((p::user_id, p::repo_id))
.do_update()
.set(p::user_id.eq(excluded(p::user_id)))
.execute(&mut db)
.await?;
}
if let Some(TypedHeader(AcceptJson(true))) = accept_json {
Ok(Json(()).into_response())
} else {
Ok(Redirect::to(&format!("/{}/{}/admin", tree.owner, tree.repo)).into_response())
}
}
#[derive(Debug, Deserialize)]
struct TagForm {
token: String,
id: Option<uuid::Uuid>,
tag_name: String,
colorp: i32,
}
#[debug_handler]
async fn tag(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<TreePath>,
Form(form): Form<TagForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
debug!("new {:?}", tree);
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
let (id, _) = crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
Some(uid),
Perm::EDIT_TAGS,
)
.await?;
use crate::db::tags::dsl as t;
if let Some(tag_id) = form.id {
diesel::update(t::tags.find(tag_id).filter(t::repository_id.eq(id)))
.set((t::name.eq(form.tag_name), t::color.eq(form.colorp)))
.execute(&mut db)
.await?;
} else {
diesel::insert_into(t::tags)
.values((
t::repository_id.eq(id),
t::name.eq(form.tag_name),
t::color.eq(form.colorp),
))
.execute(&mut db)
.await?;
}
if let Some(TypedHeader(AcceptJson(true))) = accept_json {
Ok(Json(()).into_response())
} else {
Ok(Redirect::to(&format!("/{}/{}/admin", tree.owner, tree.repo)).into_response())
}
}
use crate::repository::*;
use ::replication::Update;
use libpijul::pristine::sanakirja::MutTxn;
use libpijul::{ChannelMutTxnT, MutTxnT, MutTxnTExt, TxnT, TxnTExt};
use std::pin::Pin;
use thiserror::*;
use tracing::*;
#[derive(Clone)]
pub struct H {
locks: RepositoryLocks,
db: crate::config::Db,
}
impl H {
pub fn new(locks: RepositoryLocks, db: crate::config::Db) -> Self {
H { locks, db }
}
}
use libpijul::pristine::sanakirja::SanakirjaError;
use libpijul::pristine::{ForkError, TxnErr};
use libpijul::{ApplyError, UnrecordError};
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Sk(#[from] SanakirjaError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Txn(#[from] TxnErr<SanakirjaError>),
#[error(transparent)]
Fork(#[from] ForkError<SanakirjaError>),
#[error(transparent)]
Unrec(#[from] UnrecordError<crate::repository::changestore::Error, MutTxn<()>>),
#[error(transparent)]
Apply(#[from] ApplyError<crate::repository::changestore::Error, MutTxn<()>>),
#[error("Join error")]
Join,
#[error(transparent)]
Diesel(#[from] diesel::result::Error),
}
impl ::replication::Handler for H {
type Error = Error;
type F =
Pin<Box<dyn futures::Future<Output = Result<Option<Self::Error>, Self::Error>> + Send>>;
fn update(&self, is_source: bool, update: replication::Update) -> Self::F {
let s = self.clone();
Box::pin(async move {
match update {
Update::Change { .. } => {}
Update::Apply {
repo,
channel,
hash,
} => {
let repo_ = s.locks.get(&repo).await.unwrap();
match tokio::task::spawn_blocking(move || {
let pri = repo_.pristine.blocking_write();
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
let channel_ = txn.open_or_create_channel(&channel)?;
let result =
txn.apply_change_rec(&repo_.changes, &mut *channel_.write(), &hash);
match result {
Err(libpijul::ApplyError::LocalChange(
libpijul::LocalApplyError::ChangeAlreadyOnChannel { .. },
)) => {
error!(
"ignored error, apply {:?} to {:?}:{:?} = {:?}",
hash, repo, channel, result
);
return Ok(None);
}
Err(libpijul::ApplyError::LocalChange(
libpijul::LocalApplyError::DependencyMissing { hash },
)) => {
error!(
"apply {:?} to {:?}:{:?} = {:?}",
hash, repo, channel, result
);
return Ok(Some(
libpijul::ApplyError::LocalChange(
libpijul::LocalApplyError::DependencyMissing { hash },
)
.into(),
));
}
Ok(_) => {
debug!("apply {:?} to {:?}:{:?}", hash, repo, channel);
}
Err(e) => {
error!("apply {:?} to {:?}:{:?} = {:?}", hash, repo, channel, e);
return Err(e.into());
}
}
txn.touch_channel(&mut *channel_.write(), None);
txn.commit()?;
Ok::<_, Error>(None)
})
.await
{
Ok(Ok(Some(x))) => return Ok(Some(x)),
Ok(Ok(None)) => {}
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(Error::Join),
}
}
Update::Unrecord {
repo,
channel,
hash,
..
} => {
let repo_ = s.locks.get(&repo).await.unwrap();
match tokio::task::spawn_blocking(move || {
let pri = repo_.pristine.blocking_write();
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
let mut channel_ = txn.open_or_create_channel(&channel)?;
let result = txn.unrecord(&repo_.changes, &mut channel_, &hash, 0);
match result {
Err(libpijul::UnrecordError::ChangeNotInChannel { .. }) | Ok(_) => {
debug!("unrecord {:?} to {:?}:{:?}", hash, repo, channel);
}
Err(e) => {
error!("unrecord {:?} to {:?}:{:?} = {:?}", hash, repo, channel, e);
return Err(e.into());
}
}
let mut in_other_channels = false;
for ch in txn.channels("")? {
if txn.get_revchanges(&ch, &hash.into())?.is_some() {
in_other_channels = true;
break;
}
}
txn.touch_channel(&mut *channel_.write(), None);
txn.commit()?;
Ok::<_, Error>(in_other_channels)
})
.await
{
Ok(Ok(false)) => {
let mut p = crate::repository::nest_changes_path(&s.locks.config, repo);
libpijul::changestore::filesystem::push_filename(&mut p, &hash);
if let Ok(meta) = tokio::fs::metadata(&p).await {
std::fs::remove_file(&p).unwrap_or(());
std::fs::remove_dir(p.parent().unwrap()).unwrap_or(());
if is_source {
crate::repository::free_used_storage(
&mut *s.db.get().await.unwrap(),
repo,
meta.len(),
)
.await
.unwrap();
}
}
}
Ok(Ok(_)) => {}
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(Error::Join),
}
}
Update::NewChannel { repo, channel } => {
let repo_ = s.locks.get(&repo).await.unwrap();
let pri = repo_.pristine.write().await;
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
txn.open_or_create_channel(&channel)?;
txn.commit()?;
}
Update::Fork { repo, channel, new } => {
let repo_ = s.locks.get(&repo).await.unwrap();
let pri = repo_.pristine.write().await;
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
let chan = txn.open_or_create_channel(&channel)?;
let new = format!("{}_{}", repo, new);
match txn.fork(&chan, &new) {
Ok(_) => txn.commit()?,
Err(ForkError::ChannelNameExists(_)) => {}
Err(e) => return Err(e.into()),
}
}
Update::Prune { repo, channel } => {
let repo_ = s.locks.get(&repo).await.unwrap();
let pri = repo_.pristine.write().await;
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
txn.drop_channel(&channel)?;
txn.commit()?;
}
Update::Rename { repo, channel, new } => {
let repo_ = s.locks.get(&repo).await.unwrap();
let pri = repo_.pristine.write().await;
let mut txn = pri.mut_txn_begin()?;
let channel = format!("{}_{}", repo, channel);
if let Some(mut c) = txn.load_channel(&channel)? {
let new = format!("{}_{}", repo, new);
debug!("rename {:?} to {:?}", channel, new);
txn.rename_channel(&mut c, &new)?;
txn.commit()?;
} else {
debug!("not rename");
}
}
}
Ok(None)
})
}
}
use axum::{extract::State, http};
use http_body_util::BodyExt;
use hyper::header::*;
use tracing::*;
pub const HSTS: &str = "max-age=31536000; includeSubDomains";
pub struct BodyBuilder {
pub http: http::response::Builder,
pub html: bool,
}
type Body = http_body_util::Full<bytes::Bytes>;
impl BodyBuilder {
fn body(self, body: bytes::Bytes) -> Result<hyper::Response<Body>, http::Error> {
let html =
self.html
|| (self.http.headers_ref().map(|m| {
m.get(CONTENT_TYPE).and_then(|x| x.to_str().ok()) == Some("text/html")
}) == Some(true));
let data = if html {
minify_html::minify(&body, &minify_html::Cfg::default()).into()
} else {
body
};
Ok(self.http.body(data.into())?)
}
}
#[axum::debug_handler]
pub async fn node_proxy_resp(
State(config): State<crate::Config>,
req: axum::extract::Request,
) -> hyper::Response<Body> {
node_proxy(&config, req).await.unwrap()
}
pub async fn node_proxy(
config: &crate::config::Config,
req: axum::extract::Request,
) -> Result<hyper::Response<Body>, axum::http::Error> {
match node_proxy_(config, req).await {
Ok(resp) => {
let (mut parts, body) = resp.into_parts();
let body = body.collect().await.unwrap().to_bytes();
parts.headers.insert(
hyper::header::STRICT_TRANSPORT_SECURITY,
HSTS.try_into().unwrap(),
);
let html = parts
.headers
.get(CONTENT_TYPE)
.and_then(|x| x.to_str().ok())
== Some("text/html");
let data = if html {
minify_html::minify(&body, &minify_html::Cfg::default()).into()
} else {
body
};
let len = data.len();
let mut resp = hyper::Response::new(data.into());
*resp.status_mut() = parts.status;
let h = resp.headers_mut();
for (k, v) in parts.headers {
h.insert(k.unwrap(), v);
}
h.insert(
hyper::header::CONTENT_LENGTH,
len.to_string().parse().unwrap(),
);
Ok(resp)
}
Err(e) => {
error!("proxy error {:?}", e);
let resp = http::response::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(http::header::STRICT_TRANSPORT_SECURITY, HSTS);
Ok(BodyBuilder {
http: resp,
html: false,
}
.body("".into())?)
}
}
}
pub async fn node_proxy_(
config: &crate::config::Config,
req: axum::extract::Request,
) -> Result<hyper::Response<hyper::body::Incoming>, crate::Error> {
let mut sender = if let Some(ref svelte_socket) = config.svelte_socket {
use hyper_util::rt::TokioIo;
use tokio::net::UnixStream;
let stream = UnixStream::connect(svelte_socket).await?;
let io = TokioIo::new(stream);
let (sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(async move {
if let Err(err) = conn.await {
error!("Connection failed: {:?}", err);
}
});
sender
} else {
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
let stream = TcpStream::connect("localhost:5173").await?;
let io = TokioIo::new(stream);
let (sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(async move {
if let Err(err) = conn.await {
error!("Connection failed: {:?}", err);
}
});
sender
};
let mut req_ = hyper::Request::builder().method(req.method().clone()).uri(
hyper::Uri::builder()
.path_and_query(req.uri().path_and_query().unwrap().clone())
.build()
.unwrap(),
);
for (k, v) in req.headers() {
req_ = req_.header(k, v)
}
if cfg!(debug_assertions) {
req_ = req_.header(HOST, "localhost:5173");
} else {
req_ = req_.header(HOST, "nest.pijul.org");
}
let body = req.into_body();
let req_ = req_.body(body)?;
Ok(sender.send_request(req_).await?)
}
use bitflags::*;
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct Perm: i64 {
// General
const READ = 0x1;
// Discussions
const CREATE_DISCUSSION = 0x2;
const EDIT_DISCUSSION = 0x4;
const TAG_DISCUSSION = 0x8;
// Changes
const APPLY = 0x10;
const EDIT_CHANNELS = 0x20;
// Admin
const EDIT_TAGS = 0x40;
const EDIT_PERMISSIONS = 0x80;
}
}
impl Perm {
pub fn create_public() -> Self {
Perm::READ | Perm::CREATE_DISCUSSION
}
pub fn create_owner() -> Self {
Perm::all()
}
pub fn from_opt(i: Option<i64>) -> Self {
i.map(Perm::from_bits_truncate).unwrap_or(Perm::empty())
}
}
use lazy_static::lazy_static;
use pulldown_cmark::*;
use regex::Regex;
use tracing::debug;
lazy_static! {
static ref REPLACE: Regex = Regex::new(
r#"(#([A-Za-z0-9]+))|(@([A-Za-z0-9_-]+))|(http://\S+[[:alnum:]])|(https://\S+[[:alnum:]])"#
)
.unwrap();
}
pub struct Rendered {
pub rendered: String,
}
pub fn render_markdown(owner: &str, repo: &str, s: &str) -> Rendered {
let mut options = Options::empty();
options.insert(
Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS
| Options::ENABLE_SMART_PUNCTUATION,
);
let mut html_output = String::new();
let mut it = P {
p: Parser::new_ext(s, options),
next: [None, None, None],
owner,
repo,
current_text: None,
mentions: Vec::new(),
};
html::push_html(&mut html_output, &mut it);
Rendered {
rendered: ammonia::clean(&html_output),
}
}
struct P<'a> {
p: Parser<'a>,
next: [Option<Event<'a>>; 3],
current_text: Option<(CowStr<'a>, usize)>,
owner: &'a str,
repo: &'a str,
mentions: Vec<String>,
}
impl<'a> Iterator for P<'a> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
debug!("{:?} {:?}", self.next, self.current_text);
if let Some(next) = self.next[0].take() {
return Some(next);
}
if let Some(next) = self.next[1].take() {
return Some(next);
}
if let Some(next) = self.next[2].take() {
return Some(next);
}
if let Some((c, i)) = self.current_text.take() {
let cap = self.make_next(&c, i);
if let Some((start, end)) = cap {
let result = if start > 0 {
let t = c.split_at(i).1.split_at(start).0;
Some(Event::Text(t.to_string().into()))
} else {
None
};
if i + end < c.len() {
self.current_text = Some((c, i + end));
}
if result.is_some() {
return result;
}
} else {
let d = c.split_at(i).1;
debug!("return {:?}", d);
return Some(Event::Text(d.to_string().into()));
}
} else {
match self.p.next() {
None => return None,
Some(Event::Html(t)) => return Some(Event::Text(t)),
Some(Event::Text(t)) => self.current_text = Some((t, 0)),
Some(e) => return Some(e),
}
}
}
}
}
impl<'a> P<'a> {
fn make_next(&mut self, c: &str, i: usize) -> Option<(usize, usize)> {
let d = c.split_at(i).1;
debug!("d = {:?}", d);
let cap = if let Some(cap) = REPLACE.captures(d) {
cap
} else {
return None;
};
debug!("cap = {:?}", cap);
let mut l = None;
if let Some(disc) = cap.get(2) {
if disc.as_str().chars().all(|c| c >= '0' && c <= '9') {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!(
"/{}/{}/discussions/{}",
self.owner,
self.repo,
disc.as_str()
)
.into(),
title: "".into(),
id: "".into(),
});
} else {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!("/{}/{}/changes/{}", self.owner, self.repo, disc.as_str())
.into(),
title: "".into(),
id: "".into(),
});
}
} else if let Some(link) = cap.get(4) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!("/{}", link.as_str()).into(),
title: "".into(),
id: "".into(),
});
self.mentions.push(link.as_str().to_string())
} else if let Some(http) = cap.get(5) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: http.as_str().to_string().into(),
title: "".into(),
id: "".into(),
});
} else if let Some(http) = cap.get(6) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: http.as_str().to_string().into(),
title: "".into(),
id: "".into(),
});
}
self.next[0] = l.clone().map(Event::Start);
self.next[1] = Some(Event::Text(cap.get(0).unwrap().as_str().to_string().into()));
self.next[2] = Some(Event::End(TagEnd::Link));
let z = cap.get(0).unwrap();
Some((z.start(), z.end()))
}
}
#![warn(unused_extern_crates)]
use axum::{
debug_handler,
extract::{MatchedPath, Path, Request, State},
http::StatusCode,
response::{IntoResponse, Redirect, Response},
routing::{any, get, post},
Json, Router,
};
use axum_extra::extract::cookie::SignedCookieJar;
use axum_response_cache::CacheLayer;
use diesel::{ExpressionMethods, NullableExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::*;
use serde::*;
use std::collections::BTreeMap;
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::*;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod auth;
mod change;
mod channel;
mod config;
mod config_file;
mod db;
mod discussions;
mod email;
mod hooks;
mod identicon;
mod markdown;
mod permissions;
mod proxy;
mod replication;
mod repository;
mod settings;
mod ssh;
#[derive(Clone)]
pub struct Config {
config: Arc<config::Config>,
repo_locks: repository::RepositoryLocks,
replicator: ::replication::Worker<replication::H>,
}
impl axum::extract::FromRef<Config> for axum_csrf::CsrfConfig {
fn from_ref(c: &Config) -> Self {
c.config.csrf.clone()
}
}
impl std::ops::Deref for Config {
type Target = config::Config;
fn deref(&self) -> &Self::Target {
&self.config
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
format!(
"{}=debug,tower_http=debug,axum::rejection=trace",
env!("CARGO_CRATE_NAME")
)
.into()
}),
)
.with(tracing_subscriber::fmt::layer())
.init();
let (config_file, replication_config) = config::from_app();
let config = Arc::new(config::from_file(&config_file).await);
let repo_locks = repository::RepositoryLocks::new(config.clone());
let cors = CorsLayer::new().allow_origin(tower_http::cors::Any);
let (blrecv, replicator) = {
let (blsend, blrecv) = tokio::sync::mpsc::unbounded_channel();
let rconfig = replication_config.to_config(blsend).await;
let handler = crate::replication::H::new(repo_locks.clone(), config.db.clone());
(
blrecv,
::replication::Worker::new(Arc::new(rconfig), handler),
)
};
let cached = Router::new()
.route("/static/{*wildcard}", any(proxy::node_proxy_resp))
.route("/_app/immutable/{*wildcard}", any(proxy::node_proxy_resp))
.nest("/identicon", identicon::identicon())
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new());
debug!("cached");
let app: Router<Config> = Router::new();
let app = app
.route("/login", post(auth::login))
.route(
"/register",
post(auth::register_post).get(auth::register_get),
)
.route(
"/recover/init",
post(auth::recover_init)
)
.route(
"/recover/reset",
post(auth::recover_reset)
)
.nest("/api", api_router())
.route("/", any(root))
.merge(if cfg!(debug_assertions) {
cached
} else {
cached.layer(CacheLayer::with_lifespan(3600))
})
.fallback(proxy::node_proxy_resp)
.layer(cors)
.layer(
TraceLayer::new_for_http()
.make_span_with(|req: &Request| {
let method = req.method();
let uri = req.uri();
let matched_path = req
.extensions()
.get::<MatchedPath>()
.map(|matched_path| matched_path.as_str());
tracing::debug_span!("request", %method, %uri, matched_path)
})
.on_failure(()),
)
.with_state(Config {
config: config.clone(),
repo_locks: repo_locks.clone(),
replicator: replicator.clone(),
});
let addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
let ssh_addr = std::net::SocketAddr::V6(SocketAddrV6::new(addr, config_file.ssh.port, 0, 0));
let ssh_worker = tokio::spawn(ssh::worker(
Config {
config: config.clone(),
repo_locks: repo_locks.clone(),
replicator: replicator.clone(),
},
ssh::socket(&ssh_addr).await,
));
let tls_config = config::make_tls().await.unwrap();
let http_addr = SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
config_file.http.http_port,
0,
0,
));
let https_addr = SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
config_file.http.https_port,
0,
0,
));
tracing::debug!("listening on {}", https_addr);
let https_worker = tokio::spawn(
axum_server::bind_rustls(https_addr, tls_config).serve(
app.clone()
.into_make_service_with_connect_info::<SocketAddr>(),
),
);
tracing::debug!("listening on {}", http_addr);
let http_worker = tokio::spawn(redirect_http_to_https(
config_file.http.http_port,
config_file.http.https_port,
app,
));
config::drop_privileges(&config_file);
tokio::select!(
x = http_worker => {
error!("http worker stopped: {:?}", x);
std::process::exit(1)
},
x = https_worker => {
error!("https worker stopped: {:?}", x);
std::process::exit(2)
},
x = ssh_worker => {
error!("ssh worker stopped: {:?}", x);
std::process::exit(3)
},
x = replicator.worker(blrecv) => {
error!("replicator worker stopped: {:?}", x);
std::process::exit(6)
}
)
}
// Avoid an infinite loop between Node and Rust.
#[debug_handler]
async fn root(
State(config): State<Config>,
jar: SignedCookieJar,
req: axum::extract::Request,
) -> Result<Response, crate::Error> {
if let Some((_, login)) = get_user_login(&jar, &config).await? {
return Ok(Redirect::to(&format!("/{}", login)).into_response());
}
Ok(proxy::node_proxy(&config, req)
.await?
.into_response())
}
pub async fn redirect_http_to_https(http: u16, https: u16, mut app: Router<()>) {
fn make_https(
host: String,
uri: http::Uri,
http: u16,
https: u16,
) -> Result<http::Uri, axum::BoxError> {
let mut parts = uri.into_parts();
parts.scheme = Some(axum::http::uri::Scheme::HTTPS);
if parts.path_and_query.is_none() {
parts.path_and_query = Some("/".parse().unwrap());
}
let https_host = host.replace(&http.to_string(), &https.to_string());
parts.authority = Some(https_host.parse()?);
Ok(http::Uri::from_parts(parts)?)
}
use axum::extract::ConnectInfo;
use axum::response::IntoResponse;
use axum_extra::extract::Host;
let redirect = move |Host(host): Host,
uri: http::Uri,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
r: http::Request<axum::body::Body>| async move {
debug!("redirect {:?}", addr);
if addr.ip().is_loopback() {
debug!("is loopback");
use tower_service::Service;
Ok(app.call(r).await.into_response())
} else {
debug!("not loopback");
match make_https(host, uri, http, https) {
Ok(uri) => Ok(Redirect::permanent(&uri.to_string()).into_response()),
Err(error) => {
tracing::warn!(%error, "failed to convert URI to HTTPS");
Err(StatusCode::BAD_REQUEST.into_response())
}
}
}
};
let addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
let http_addr = SocketAddr::V6(SocketAddrV6::new(addr, http, 0, 0));
let listener = tokio::net::TcpListener::bind(http_addr).await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
use axum::handler::HandlerWithoutStateExt;
axum::serve(
listener,
redirect.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}
#[derive(Debug, Serialize)]
struct User {
user: String,
login: Option<String>,
repos: BTreeMap<String, Repo>,
is_owner: bool,
}
#[derive(Debug, Serialize)]
struct Repo {
private: bool,
}
#[derive(Debug, Serialize)]
struct UserId {
id: uuid::Uuid,
login: String,
}
#[debug_handler]
async fn user_id(
State(config): State<Config>,
jar: SignedCookieJar,
) -> (SignedCookieJar, Json<Option<UserId>>) {
if let Ok(Some((id, login))) = get_user_login(&jar, &config).await {
(jar, Json(Some(UserId { login, id })))
} else {
use axum_extra::extract::cookie::Cookie;
(jar.remove(Cookie::from("session_id")), Json(None))
}
}
#[debug_handler]
async fn user(
State(config): State<Config>,
jar: SignedCookieJar,
Path(user): Path<String>,
) -> Result<Response, Error> {
debug!("user {:?}", user);
use db::repositories::dsl as r;
use db::users::dsl as u;
let (user_id, login) = if let Some((id, login)) = get_user_login(&jar, &config).await? {
(id, Some(login))
} else {
(uuid::Uuid::nil(), None)
};
let repos = u::users
.left_join(r::repositories)
.select((u::id, r::name.nullable(), permissions!(user_id, r::id), permissions!(uuid::Uuid::nil(), r::id)))
.filter(u::login.eq(&user))
.filter(u::email_is_invalid.is_null())
.get_results::<(uuid::Uuid, Option<String>, i64, i64)>(&mut config.db.get().await?)
.await?;
debug!("repos = {:?}", repos);
if repos.is_empty() {
Ok((StatusCode::NOT_FOUND, format!("{{}}")).into_response())
} else {
let mut uid = uuid::Uuid::nil();
let repos = repos
.into_iter()
.filter_map(|(id, name, perm, perm_all)| {
uid = id;
use crate::permissions::Perm;
let perm = Perm::from_bits(perm).unwrap();
let perm_all = Perm::from_bits(perm_all).unwrap();
if perm.contains(Perm::READ) {
name.map(|name| (name, Repo { private: !perm_all.contains(Perm::READ) }))
} else {
None
}
})
.collect();
let is_owner = Some(uid) == get_user_id(&jar)?;
debug!("uid = {:?}", uid);
let resp = User {
user,
login,
repos,
is_owner,
};
debug!("{:?}", resp);
Ok(Json(resp).into_response())
}
}
fn api_router() -> Router<Config> {
Router::new()
.route("/user/{login}", get(user))
.route("/user", get(user_id))
.nest("/settings", settings::router())
.route("/tree/{owner}/{repo}", get(repository::router::tree))
.nest("/admin/{owner}/{repo}", repository::admin::router())
.nest("/change", change::router())
.nest("/discussion", discussions::router())
.nest("/channel", channel::router())
.fallback(fallback)
}
async fn fallback(uri: http::Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
async fn fallback_json(uri: http::Uri) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!(r#"{{ "error": "No route for {uri}" }}"#),
)
}
fn get_user_id(jar: &SignedCookieJar) -> Result<Option<uuid::Uuid>, Error> {
if let Some(sid) = jar.get("session_id") {
let (cookie_uid, _time): (uuid::Uuid, chrono::DateTime<chrono::Utc>) =
bincode::deserialize(&data_encoding::BASE64URL.decode(sid.value().as_bytes())?)?;
Ok(Some(cookie_uid))
} else {
Ok(None)
}
}
fn get_user_id_strict(jar: &SignedCookieJar) -> Result<uuid::Uuid, Error> {
if let Some(id) = get_user_id(jar)? {
Ok(id)
} else {
Err(Error::NeedsAuth)
}
}
async fn get_user_login(
jar: &SignedCookieJar,
config: &Config,
) -> Result<Option<(uuid::Uuid, String)>, Error> {
use db::users::dsl as u;
if let Some(id) = get_user_id(jar)? {
if let Some(login) = u::users
.find(id)
.select(u::login)
.get_result::<String>(&mut config.db.get().await?)
.await
.optional()?
{
return Ok(Some((id, login)));
}
}
Ok(None)
}
async fn get_user_login_(
jar: &SignedCookieJar,
config: &Config,
) -> Result<(Option<uuid::Uuid>, Option<String>), Error> {
if let Some((a, b)) = get_user_login(jar, config).await? {
Ok((Some(a), Some(b)))
} else {
Ok((None, None))
}
}
async fn get_user_login_email(
jar: &SignedCookieJar,
config: &Config,
) -> Result<Option<(uuid::Uuid, String, String)>, Error> {
use db::users::dsl as u;
if let Some(id) = get_user_id(jar)? {
if let Some((login, email)) = u::users
.find(id)
.select((u::login, u::email))
.get_result::<(String, String)>(&mut config.db.get().await?)
.await
.optional()?
{
return Ok(Some((id, login, email)));
}
}
Ok(None)
}
async fn get_user_login_email_strict(
jar: &SignedCookieJar,
config: &Config,
) -> Result<(uuid::Uuid, String, String), Error> {
if let Some(id) = get_user_login_email(jar, config).await? {
Ok(id)
} else {
Err(Error::NeedsAuth)
}
}
async fn get_user_login_strict(
jar: &SignedCookieJar,
config: &Config,
) -> Result<(uuid::Uuid, String), Error> {
if let Some(id) = get_user_login(jar, config).await? {
Ok(id)
} else {
Err(Error::NeedsAuth)
}
}
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Lock")]
Lock,
#[error("Depended upon")]
DependedUpon,
#[error(transparent)]
SSH(#[from] thrussh::Error),
#[error(transparent)]
Hyper(#[from] hyper::Error),
#[error(transparent)]
Http(#[from] http::Error),
#[error(transparent)]
Join(#[from] tokio::task::JoinError),
#[error("Repository not found")]
RepositoryNotFound,
#[error("Unknown Pijul error")]
Txn,
#[error(transparent)]
Archive(
#[from]
libpijul::output::ArchiveError<
repository::changestore::Error,
libpijul::pristine::sanakirja::GenericTxn<sanakirja::MutTxn<Arc<sanakirja::Env>, ()>>,
std::io::Error,
>,
),
#[error(transparent)]
Apply(
#[from]
libpijul::ApplyError<
repository::changestore::Error,
libpijul::pristine::sanakirja::GenericTxn<sanakirja::MutTxn<Arc<sanakirja::Env>, ()>>,
>,
),
#[error(transparent)]
PijulChange(#[from] libpijul::change::ChangeError),
#[error("Hash parse error: {hash:?}")]
HashParse { hash: String },
#[error("Insufficient permissions, required {required:?}, got {got:?}")]
Permissions {
required: permissions::Perm,
got: permissions::Perm,
},
#[error("Auth required")]
NeedsAuth,
#[error(transparent)]
Csrf(#[from] axum_csrf::CsrfError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Decode(#[from] data_encoding::DecodeError),
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error(transparent)]
ChangeStore(#[from] repository::changestore::Error),
#[error(transparent)]
Sanakirja(#[from] libpijul::pristine::sanakirja::SanakirjaError),
#[error("The request body was too large, please retry with something smaller.")]
BodyTooLarge,
#[error("Wrong token")]
WrongToken,
#[error("Change not found")]
ChangeNotFound,
#[error("Channel not found: {:?}", channel)]
ChannelNotFound { channel: String },
#[error("Inactive user")]
InactiveUser,
#[error("OAuth parse")]
OAuthParse,
#[error("Wrong signature")]
WrongSignature,
#[error("The last channel in a repository cannot be deleted")]
LastChannelCannotBeDeleted,
#[error("Protocol error")]
ProtocolError,
#[error("Not enough space (max {:?})", quota)]
Quota { quota: u64 },
#[error("Forbidden")]
Forbidden,
#[error("Ambiguous path")]
AmbiguousInode,
#[error("Path not found")]
InodeNotFound,
#[error("This channel has changed independently")]
ConcurrentChannelOps,
#[error(transparent)]
Diesel(#[from] diesel::result::Error),
#[error(transparent)]
Deadpool(#[from] deadpool::managed::PoolError<diesel_async::pooled_connection::PoolError>),
#[error(transparent)]
Replication(#[from] ::replication::Error<replication::Error>),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
ParseInt(#[from] std::num::ParseIntError),
#[error(transparent)]
Time(#[from] std::time::SystemTimeError),
#[error(transparent)]
Bs58(#[from] bs58::decode::Error),
#[error(transparent)]
Key(#[from] libpijul::key::KeyError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Rusoto(#[from] rusoto_core::RusotoError<rusoto_ses::SendEmailError>),
#[error(transparent)]
Url(#[from] serde_urlencoded::de::Error),
}
impl<E: Into<Error> + std::error::Error> From<libpijul::pristine::TxnErr<E>> for Error {
fn from(e: libpijul::pristine::TxnErr<E>) -> Self {
e.0.into()
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
debug!("response {:?}", self);
match self {
Error::RepositoryNotFound => (StatusCode::NOT_FOUND, "{}").into_response(),
Error::ChannelNotFound { .. } => (StatusCode::NOT_FOUND, "{}").into_response(),
Error::InodeNotFound => (StatusCode::NOT_FOUND, "{}").into_response(),
Error::Csrf(_) => (StatusCode::FORBIDDEN, "{}").into_response(),
Error::NeedsAuth => (StatusCode::FORBIDDEN, "{}").into_response(),
Error::Permissions { .. } => (StatusCode::FORBIDDEN, "{}").into_response(),
_ => (StatusCode::INTERNAL_SERVER_ERROR, "{}").into_response(),
}
}
}
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "keyalgorithm"))]
pub struct Keyalgorithm;
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
diesel_derive_enum::DbEnum,
)]
#[ExistingTypePath = "Keyalgorithm"]
#[DbValueStyle = "PascalCase"]
pub enum Keyalgorithm_ {
Ed25519,
}
#[macro_export]
macro_rules! permissions {
( $u:expr, $r: expr ) => {{
use diesel::sql_types::{BigInt, Uuid};
diesel::dsl::sql::<BigInt>("permissions(")
.bind::<Uuid, _>($u)
.sql(", ")
.bind::<Uuid, _>($r)
.sql(")")
}};
}
#[macro_export]
macro_rules! has_permissions {
( $u:expr, $r: expr, $p: expr ) => {{
use diesel::sql_types::{BigInt, Bool, Uuid};
diesel::dsl::sql::<Bool>("permissions(")
.bind::<Uuid, _>($u)
.sql(", ")
.bind::<Uuid, _>($r)
.sql(") & ")
.bind::<BigInt, _>($p)
.sql(" != 0")
}};
}
pub fn is_valid_name(u: &str) -> bool {
u.chars().all(|x| {
(x >= 'A' && x <= 'Z')
|| (x >= 'a' && x <= 'z')
|| (x >= '0' && x <= '9')
|| (x == '-' || x == '_' || x == '.')
})
}
pub fn is_valid_repo_channel_name(u: &str) -> bool {
u.chars().all(|x| {
(x >= 'A' && x <= 'Z')
|| (x >= 'a' && x <= 'z')
|| (x >= '0' && x <= '9')
|| (x == '-' || x == '_' || x == '.' || x == ':' || x == ' ')
})
}
pub fn split_repo_channel<'a>(
repo: &'a str,
) -> (std::borrow::Cow<'a, str>, Option<std::borrow::Cow<'a, str>>) {
use percent_encoding::*;
let mut s = repo.split(':');
match (s.next(), s.next()) {
(Some(s), Some(b)) => (
percent_decode_str(s).decode_utf8_lossy(),
Some(percent_decode_str(b).decode_utf8_lossy()),
),
_ => (percent_decode_str(repo).decode_utf8_lossy(), None),
}
}
use crate::proxy::HSTS;
use crate::Config;
use axum::extract::{Path, State};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use hyper;
use hyper::header::*;
use hyper::StatusCode;
use rand::distr::StandardUniform;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use serde_derive::*;
use std;
use std::f64::consts::SQRT_2;
use std::io::Write;
use tracing::*;
const N_POINTS: usize = 10;
const MARGIN: f64 = 10.;
const SCALE: f64 = 200.;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Size {
Full,
Small,
}
const QUESTION: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle" viewBox="1 1 14 14">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>"#;
pub fn identicon() -> axum::Router<Config> {
axum::Router::new()
.route("/{login}", axum::routing::get(identicon_full))
.route("/{login}/small", axum::routing::get(identicon_small))
}
#[derive(Debug, Deserialize)]
struct Identicon {
login: String,
}
type Body = http_body_util::Full<bytes::Bytes>;
#[axum::debug_handler]
async fn identicon_full(
config: State<Config>,
name: Path<Identicon>,
req: axum::extract::Request,
) -> hyper::Response<Body> {
identicon_(Size::Full, config, name, req).await.unwrap()
}
#[axum::debug_handler]
async fn identicon_small(
config: State<Config>,
name: Path<Identicon>,
req: axum::extract::Request,
) -> hyper::Response<Body> {
identicon_(Size::Small, config, name, req).await.unwrap()
}
async fn identicon_(
size: Size,
State(config): State<Config>,
Path(name): Path<Identicon>,
req: axum::extract::Request,
/*
name: String,
req: &hyper::Request<hyper::body::Body>,
*/
) -> Result<hyper::Response<Body>, crate::Error> {
use crate::db::users::dsl as users;
let mut db_ = config.db.get().await.unwrap();
let uid = if let Some(user) = users::users
.filter(users::login.eq(&name.login))
.select(users::id)
.get_result::<uuid::Uuid>(&mut db_)
.await
.optional()
.unwrap()
{
user
} else {
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
let now = std::time::SystemTime::now();
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, &config.version_time_str)
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
return Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(LAST_MODIFIED, config.version_time_str.as_str())
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(CONTENT_TYPE, "image/svg+xml")
.body(QUESTION.into())?);
};
debug!("id: {:?}", uid);
use crate::db::profile_pics::dsl as profile_pics;
if let Some((mime, bytes, modif)) = (if let Size::Full = size {
profile_pics::profile_pics
.find(uid)
.select((
profile_pics::format,
profile_pics::image,
profile_pics::updated,
))
.get_result(&mut db_)
} else {
profile_pics::profile_pics
.find(uid)
.select((
profile_pics::format,
profile_pics::small,
profile_pics::updated,
))
.get_result::<(String, Vec<u8>, chrono::DateTime<chrono::Utc>)>(&mut db_)
})
.await
.optional()?
{
let modif: std::time::SystemTime = modif.into();
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if modif > ifmodified {
let now = std::time::SystemTime::now();
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
return Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(CONTENT_TYPE, &mime)
.header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
.body(bytes.into())?);
}
let now = std::time::SystemTime::now();
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, &config.version_time_str)
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(LAST_MODIFIED, config.version_time_str.as_str())
.header(CONTENT_TYPE, "image/svg+xml")
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(DATE, httpdate::fmt_http_date(now))
.body(make_identicon(name.login.as_bytes()).into())?)
}
fn make_identicon(user: &[u8]) -> Vec<u8> {
let seed = seed_bytes(user);
let rng: ChaChaRng = SeedableRng::from_seed(seed);
let mut points = [(0., 0.); N_POINTS];
for ((x, y), p) in rng
.sample_iter(&StandardUniform)
.scan(
(0., 0.),
|&mut (ref mut x0, ref mut y0), (x, y): (f64, f64)| {
let x: f64 = (x * 2.) - 1.;
let y: f64 = (y * 2.) - 1.;
let d = (x * x + y * y).sqrt();
*x0 = *x0 + x / d;
*y0 = *y0 + y / d;
Some((*x0, *y0))
},
)
.zip(points.iter_mut())
{
*p = (x, y)
}
// Compute bounding box.
let (extr_x, extr_y) = bezier_extr(points[0], points[1], points[2]);
let mut min_x = points[0].0.min(points[2].0).min(extr_x);
let mut min_y = points[0].1.min(points[2].1).min(extr_y);
let mut max_x = points[0].0.max(points[2].0).max(extr_x);
let mut max_y = points[0].1.max(points[2].1).max(extr_y);
// Since we're using splines, "mid" is the middle control
// point in the quadratic Bezier curve, at each step,
// starting at step one.
let mut mid = points[1];
for (&(x0, y0), &(x1, y1)) in points.iter().skip(2).zip(points.iter().skip(3)) {
mid.0 = x0 + (x0 - mid.0);
mid.1 = y0 + (y0 - mid.1);
let (m0, m1) = bezier_extr((x0, y0), mid, (x1, y1));
min_x = min_x.min(m0).min(x1);
min_y = min_y.min(m1).min(y1);
max_x = max_x.max(m0).max(x1);
max_y = max_y.max(m1).max(y1);
}
let max = (max_x - min_x).max(max_y - min_y);
let scale = SQRT_2 * max;
let margin_x = (max - (max_x - min_x)) / 2. - min_x;
let margin_y = (max - (max_y - min_y)) / 2. - min_y;
for &mut (ref mut x, ref mut y) in points.iter_mut() {
*x = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_x + *x) / scale));
*y = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_y + *y) / scale));
}
let mut s:Vec<u8> = format!(r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision"><path d=""#, SCALE + MARGIN * 2., SCALE + MARGIN * 2.).into();
write!(
s,
"M {} {} Q {} {}, {} {} ",
points[0].0.round(),
points[0].1.round(),
points[1].0.round(),
points[1].1.round(),
points[2].0.round(),
points[2].1.round()
)
.unwrap();
for &(x, y) in points.iter().skip(3) {
write!(s, "T {} {} ", x.round(), y.round()).unwrap();
}
/*
// colored signatures
let h = rng.gen::<f64>();
let color = Hsv::new(RgbHue::from(h * 360f64), 0.8, 1.);
let rgb = Rgb::from(color);
write!(s, "\" stroke=\"rgb({},{},{})\" stroke-width=\"2\" fill=\"transparent\"/></svg>",
(rgb.red * 255f64).round() as u8,
(rgb.green * 255f64).round() as u8,
(rgb.blue * 255f64).round() as u8
).unwrap();
*/
write!(
s,
"\" stroke=\"black\" stroke-width=\"3\" fill=\"transparent\"/></svg>"
)
.unwrap();
debug!("s: {:?}", std::str::from_utf8(&s));
s
}
fn bezier_extr(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> (f64, f64) {
(bezier_extr_1(a.0, b.0, c.0), bezier_extr_1(a.1, b.1, c.1))
}
fn bezier_extr_1(a: f64, b: f64, c: f64) -> f64 {
// Coefficient of the polynomial.
let u = a - 2. * b + c;
let v = 2. * (b - a);
let w = a;
let t = ((-v) / (2. * u)).min(1.).max(0.);
u * t * t + v * t + w
}
fn seed_bytes(from: &[u8]) -> [u8; 32] {
let mut u = [0; 32];
let l = from.len();
for i in 0..32 {
u[i] = from[i % l];
}
u
}
use bitflags::bitflags;
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use tracing::*;
use uuid::Uuid;
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct Action: i64 {
// Per-repository permissions.
const NEW_DISCUSSION = 0x1;
const ADD_PATCH = 0x2;
const APPLY_PATCH = 0x4;
}
}
#[derive(Debug)]
pub struct Hook {
pub url: String,
pub secret: String,
}
impl Hook {
async fn run(&self, body: String) -> Result<(), crate::Error> {
debug!("hook: {:?}", self);
let client = reqwest::Client::new();
let mut req = client.post(&self.url);
if let Ok(url) = self.url.parse::<url::Url>() {
if let (Some(host), Some(port)) = (url.host(), url.port()) {
req = req.header("Host", format!("{}:{}", host, port).as_str());
}
}
let signature = {
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer;
let pkey = PKey::hmac(self.secret.as_bytes()).unwrap();
let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap();
signer.update(body.as_bytes()).unwrap();
signer.sign_to_vec().unwrap()
};
let s = format!("sha256={}", data_encoding::HEXLOWER.encode(&signature));
req.header("X-Nest-Event-Signature", s.as_str())
.body(body)
.send()
.await?;
Ok(())
}
}
pub async fn run_hooks_by_repo_id(
db: &mut diesel_async::AsyncPgConnection,
id: Uuid,
action: Action,
contents: &pijul_hooks::HookContent,
) -> Result<(), crate::Error> {
let body = serde_json::to_string(contents).unwrap();
use crate::db::hooks::dsl as hooks;
for (url, secret, a) in hooks::hooks
.filter(hooks::repository.eq(id))
.select((hooks::url, hooks::secret, hooks::action))
.get_results::<(String, String, Option<i64>)>(db)
.await?
{
if let Some(a) = a {
if (action.bits() & a) != 0 {
(Hook { url, secret }).run(body.clone()).await?
}
} else {
(Hook { url, secret }).run(body.clone()).await?
}
}
Ok(())
}
use crate::Config;
use rusoto_core;
use rusoto_ses;
use tracing::*;
pub const A_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("color:#007bff;text-decoration:none;");
pub const CONTENT_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("padding:1em;max-width:600px;margin:0 auto;");
pub const FOOTER_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("max-width:600px;margin:10px auto;font-size: small; color: #666666;");
pub const BLOCKQUOTE_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("border-left:2px solid #666666;padding-left:10px;margin:30px 0 30px 30px;");
pub async fn send_email(
config: &Config,
subject: String,
body: rusoto_ses::Body,
address: String,
) -> Result<rusoto_ses::SendEmailResponse, rusoto_core::RusotoError<rusoto_ses::SendEmailError>> {
let subject = rusoto_ses::Content {
data: subject,
charset: Some("utf-8".to_string()),
};
debug!("sending an email to {:?}", address);
let client = rusoto_core::request::HttpClient::new().unwrap();
let email = rusoto_ses::SendEmailRequest {
message: rusoto_ses::Message { body, subject },
source: config.email_source.clone(),
destination: rusoto_ses::Destination {
to_addresses: Some(vec![address]),
..rusoto_ses::Destination::default()
},
..rusoto_ses::SendEmailRequest::default()
};
use rusoto_ses::Ses;
let result =
rusoto_ses::SesClient::new_with(client, config.email.clone(), rusoto_core::Region::EuWest1)
.send_email(email)
.await;
debug!("result = {:?}", result);
result
}
{author} added a change to discussion #{disc} {name}
{message}
- View this change online: https://{host}/{owner}/{repo}/discussions/{disc}#{hash}?login={login}
—
You're receiving this email because you are subscribed to https://{host}/{owner}/{repo}/discussions/{disc}.
Unsubscribe by visiting https://{host}/{owner}/{repo}/discussions/{disc}/unsubscribe?login={login}
use crate::config::Color;
use crate::permissions::Perm;
use crate::{fallback, get_user_login, get_user_login_email, get_user_login_email_strict, Config};
use axum::{
debug_handler,
extract::{ConnectInfo, Path, Query, State},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Form, Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use axum_extra::TypedHeader;
use chrono::{DateTime, Utc};
use diesel::{
BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods,
OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncPgConnection;
use diesel_async::{AsyncConnection, RunQueryDsl};
use crate::repository::TreePath;
use http::StatusCode;
use libpijul::changestore::ChangeStore;
use libpijul::Base32;
use serde_derive::*;
use std::net::SocketAddr;
use tracing::*;
pub fn router() -> Router<Config> {
Router::new()
.route("/{owner}/{repo}", get(list))
.route("/{owner}/{repo}/new", get(new).post(new_post))
.route(
"/{owner}/{repo}/{disc}",
get(discussion).post(discussion_post),
)
.route("/{owner}/{repo}/{disc}/remove_change", post(remove_change))
.route("/{owner}/{repo}/{disc}/add_change", post(add_change))
.fallback(fallback)
}
#[derive(Debug, Deserialize)]
pub struct DiscQuery {
from: Option<u64>,
closed: Option<bool>,
q: Option<String>,
}
pub async fn list(
State(config): State<Config>,
jar: SignedCookieJar,
Path(tree): Path<TreePath>,
Query(q): Query<DiscQuery>,
) -> Result<Response, crate::Error> {
debug!("list {:?} {:?}", tree, q);
let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
(Some(a), Some(b))
} else {
(None, None)
};
let mut db = config.db.get().await?;
let (id, _) =
crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
if let Some(_) = q.q {
unimplemented!()
} else {
use crate::db::discussion_tags::dsl as dt;
use crate::db::discussions::dsl as d;
use crate::db::tags::dsl as tags;
use crate::db::users::dsl as users;
use diesel::dsl::count;
let n = d::discussions
.filter(d::repository_id.eq(id))
.select(count(d::id))
.get_result::<i64>(&mut db)
.await?;
let mut max_number = 0;
let mut disc: Vec<_> = (if let Some(true) = q.closed {
d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.select((
d::id,
d::number,
d::title,
users::login,
d::creation_date,
d::changes,
d::closed,
))
.order_by(d::number)
.offset(q.from.unwrap_or(0) as i64)
.limit(LIMIT)
.get_results::<(
uuid::Uuid,
i32,
String,
String,
DateTime<Utc>,
i32,
Option<DateTime<Utc>>,
)>(&mut db)
.await?
} else {
d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.filter(d::closed.is_null())
.select((
d::id,
d::number,
d::title,
users::login,
d::creation_date,
d::changes,
d::closed,
))
.order_by(d::number)
.offset(q.from.unwrap_or(0) as i64)
.limit(LIMIT)
.get_results::<(
uuid::Uuid,
i32,
String,
String,
DateTime<Utc>,
i32,
Option<DateTime<Utc>>,
)>(&mut db)
.await?
})
.into_iter()
.map(|(id, num, title, author, opened, changes, closed)| {
max_number = max_number.max(num);
DiscussionListItem {
id,
num,
title,
author,
opened,
changes,
closed,
..DiscussionListItem::default()
}
})
.collect();
let mut i = 0;
for (num, tag) in d::discussions
.inner_join(dt::discussion_tags)
.inner_join(tags::tags.on(dt::tag.eq(tags::id.nullable())))
.filter(d::number.ge(q.from.unwrap_or(0) as i32))
.filter(d::number.le(max_number))
.filter(tags::id.is_not_null())
.select((d::number, tags::id))
.get_results::<(i32, uuid::Uuid)>(&mut db)
.await?
{
while i < disc.len() && num < disc[i].num {
i += 1
}
if i >= disc.len() {
break;
}
disc[i].tags.push(tag)
}
Ok(Json(Discussions {
owner: tree.owner,
repo: tree.repo,
login,
discussions: disc,
n,
})
.into_response())
}
}
#[derive(Debug, Serialize)]
struct Discussions {
owner: String,
repo: String,
n: i64,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
discussions: Vec<DiscussionListItem>,
}
#[derive(Debug, Deserialize)]
pub struct DiscPath {
owner: String,
repo: String,
disc: i32,
}
const LIMIT: i64 = 20;
#[derive(Debug, Default, Serialize)]
struct DiscussionListItem {
id: uuid::Uuid,
num: i32,
title: String,
author: String,
opened: DateTime<Utc>,
changes: i32,
tags: Vec<uuid::Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
closed: Option<DateTime<Utc>>,
}
#[debug_handler]
pub async fn new(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<TreePath>,
) -> Result<Response, crate::Error> {
debug!("new {:?}", tree);
let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
(Some(a), Some(b))
} else {
(None, None)
};
let mut db = config.db.get().await?;
crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
uid,
Perm::CREATE_DISCUSSION,
)
.await?;
let auth_token = token.authenticity_token().ok();
Ok((
token,
Json(NewDiscussion {
owner: tree.owner,
repo: tree.repo,
login,
unique: rand::random(),
token: auth_token,
}),
)
.into_response())
}
#[derive(Debug, Serialize)]
struct NewDiscussion {
owner: String,
repo: String,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
unique: i64,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct NewDiscussionForm {
new_discussion_name: String,
new_discussion_comment: String,
uniq: i64,
token: String,
}
#[debug_handler]
pub async fn new_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<TreePath>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(form): Form<NewDiscussionForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
debug!("new {:?}", tree);
let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let (id, _) = crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
Some(uid),
Perm::CREATE_DISCUSSION,
)
.await?;
use crate::db::repositories::dsl as r;
db.transaction(move |mut txn| {
(async move {
let number: i32 = diesel::update(r::repositories.find(id))
.set(r::next_discussion_number.eq(r::next_discussion_number + 1))
.returning(r::next_discussion_number)
.get_result(&mut txn)
.await?;
use crate::db::discussions::dsl as d;
let ip_sql =
ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
.unwrap();
let disc_id: uuid::Uuid = diesel::insert_into(d::discussions)
.values((
d::title.eq(&form.new_discussion_name.trim()),
d::author.eq(uid),
d::creation_ip.eq(&ip_sql),
d::repository_id.eq(id),
d::number.eq(number),
d::uniq.eq(&form.uniq),
))
.returning(d::id)
.get_result(&mut txn)
.await?;
use crate::db::comments::dsl as c;
let comment = form.new_discussion_comment.trim();
if !comment.is_empty() {
let comment_html =
crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
diesel::insert_into(c::comments)
.values((
c::discussion_id.eq(disc_id),
c::author.eq(uid),
c::creation_ip.eq(&ip_sql),
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
c::uniq.eq(&form.uniq),
))
.execute(&mut txn)
.await?;
}
use crate::db::discussion_subscriptions::dsl as ds;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
.execute(&mut txn)
.await?;
if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
Ok::<_, crate::Error>(
(
token.clone(),
Json(
discussion_json(
&config,
&mut txn,
id,
token,
Path(DiscPath {
owner: tree.owner,
repo: tree.repo,
disc: number,
}),
Some(login),
Some(email),
)
.await?,
),
)
.into_response(),
)
} else {
Ok::<_, crate::Error>(
Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, number
))
.into_response(),
)
}
})
.scope_boxed()
})
.await
}
#[debug_handler]
pub async fn discussion(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
) -> Result<Response, crate::Error> {
debug!("discussion {:?}", tree);
let (uid, login, email) = if let Some((a, b, c)) = get_user_login_email(&jar, &config).await? {
(Some(a), Some(b), Some(c))
} else {
(None, None, None)
};
let mut db = config.db.get().await?;
let (repo, _) =
crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
Ok((
token.clone(),
Json(discussion_json(&config, &mut db, repo, token, Path(tree), login, email).await?),
)
.into_response())
}
async fn discussion_json(
config: &Config,
db: &mut AsyncPgConnection,
id: uuid::Uuid,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
login: Option<String>,
email: Option<String>,
) -> Result<Discussion, crate::Error> {
use crate::db::discussions::dsl as d;
use crate::db::users::dsl as users;
let (disc_id, title, author, opened, closed) = d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.filter(d::number.eq(tree.disc))
.select((d::id, d::title, users::login, d::creation_date, d::closed))
.get_result::<(_, _, _, DateTime<Utc>, Option<DateTime<Utc>>)>(db)
.await?;
use crate::db::comments::dsl as comments;
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussion_tags::dsl as dt;
use crate::db::tags::dsl as tags;
let mut comments: Vec<_> = comments::comments
.inner_join(users::users)
.filter(comments::discussion_id.eq(disc_id))
.select((
comments::id,
users::login,
comments::creation_date,
comments::contents,
comments::cached_html,
))
.order_by(comments::creation_date)
.get_results::<(uuid::Uuid, String, DateTime<Utc>, String, String)>(db)
.await?
.into_iter()
.map(
|(id, author, date, content, content_html)| DiscussionItem::Comment {
comment: Comment {
author,
id,
timestamp: date.timestamp(),
content,
content_html,
},
},
)
.collect();
for (id, name, color, date, active, author, dt_id, added) in dt::discussion_tags
.inner_join(users::users)
.left_join(tags::tags)
.filter(dt::discussion.eq(disc_id))
.select((
tags::id.nullable(),
tags::name.nullable(),
tags::color.nullable(),
dt::date,
dt::active,
users::login,
dt::id,
dt::addition,
))
.order_by(dt::date)
.get_results::<(
Option<uuid::Uuid>,
Option<String>,
Option<i32>,
DateTime<Utc>,
bool,
String,
uuid::Uuid,
bool,
)>(db)
.await?
{
let tag = if let (Some(id), Some(name), Some(color)) = (id, name, color) {
let color = Color(color);
Some(TagT {
name,
id,
color: color.to_string(),
fg: color.fg().to_string(),
active,
})
} else {
None
};
comments.push(DiscussionItem::Tag {
tag: Tag {
author,
id: dt_id,
timestamp: date.timestamp(),
tag,
added,
},
})
}
for (pushed_by, hash, timestamp, change_id, removed) in dc::discussion_changes
.inner_join(users::users)
.select((users::login, dc::change, dc::added, dc::id, dc::removed))
.get_results::<(
String,
String,
DateTime<Utc>,
uuid::Uuid,
Option<DateTime<Utc>>,
)>(db)
.await?
{
let locks = config.repo_locks.clone();
let repo = locks.get(&id).await?;
let mut header = repo
.changes
.get_header(&libpijul::Hash::from_base32(hash.as_bytes()).unwrap())?;
let hash_ = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();
comments.push(DiscussionItem::Patch {
patch: Patch {
can_add: if removed.is_some() {
can_add(&config, db, id, disc_id, hash_).await.is_ok()
} else {
false
},
can_remove: if removed.is_none() {
can_remove(&config, db, id, disc_id, hash_).await.is_ok()
} else {
false
},
authors: crate::change::get_authors(db, &mut header.authors).await?,
header,
pushed_by,
hash,
timestamp: timestamp.timestamp(),
removed: removed.as_ref().map(DateTime::timestamp),
id: change_id,
},
})
}
comments.sort_by_key(DiscussionItem::timestamp);
let auth_token = token.authenticity_token().ok();
Ok(Discussion {
owner: tree.owner,
repo: tree.repo,
// channels,
// default_channel,
login,
email,
token: auth_token,
uniq: rand::random(),
d: DiscussionT {
n: tree.disc,
id: disc_id,
title,
author,
closed: closed.map(|x| x.timestamp()),
opened: opened.timestamp(),
},
comments,
})
}
#[derive(Debug, Serialize)]
struct Discussion {
owner: String,
repo: String,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
// channels: Vec<String>,
// default_channel: String,
uniq: i64,
d: DiscussionT,
comments: Vec<DiscussionItem>,
}
#[derive(Debug, Serialize)]
struct DiscussionT {
n: i32,
id: uuid::Uuid,
title: String,
author: String,
#[serde(skip_serializing_if = "Option::is_none")]
closed: Option<i64>,
opened: i64,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum DiscussionItem {
Comment { comment: Comment },
Patch { patch: Patch },
Tag { tag: Tag },
}
impl DiscussionItem {
fn timestamp(&self) -> i64 {
match self {
DiscussionItem::Comment { comment } => comment.timestamp,
DiscussionItem::Patch { patch } => patch.timestamp,
DiscussionItem::Tag { tag } => tag.timestamp,
}
}
}
#[derive(Debug, Serialize)]
struct Comment {
author: String,
timestamp: i64,
id: uuid::Uuid,
content: String,
content_html: String,
}
#[derive(Debug, Serialize)]
struct Tag {
author: String,
timestamp: i64,
added: bool,
id: uuid::Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<TagT>,
}
#[derive(Debug, Serialize)]
struct TagT {
id: uuid::Uuid,
name: String,
fg: String,
color: String,
active: bool,
}
#[derive(Debug, Serialize)]
struct Patch {
can_add: bool,
can_remove: bool,
hash: String,
timestamp: i64,
pushed_by: String,
#[serde(skip_serializing_if = "Option::is_none")]
removed: Option<i64>,
header: libpijul::change::ChangeHeader,
authors: Vec<crate::change::Author>,
id: uuid::Uuid,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
enum Action {
#[serde(rename = "close")]
Close,
#[serde(rename = "reopen")]
Reopen,
#[serde(rename = "apply")]
Apply,
#[serde(rename = "unrecord")]
Unrecord,
}
#[derive(Debug, Deserialize)]
pub struct RemoveChangeForm {
discussion_change: uuid::Uuid,
token: String,
}
#[axum::debug_handler]
async fn add_change(
config: State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
tree: Path<DiscPath>,
form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
edit_change(true, config, jar, token, tree, form).await
}
#[axum::debug_handler]
async fn remove_change(
config: State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
tree: Path<DiscPath>,
form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
edit_change(false, config, jar, token, tree, form).await
}
async fn edit_change(
add: bool,
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
Form(form): Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
use crate::db::repositories::dsl as r;
let uid = crate::get_user_id_strict(&jar)?;
config
.db
.get()
.await?
.transaction(|mut txn| {
(async move {
// If the user has permissions, return the discussion id.
let repo_disc = dc::discussion_changes
.find(form.discussion_change)
.inner_join(d::discussions)
.inner_join(r::repositories.on(d::repository_id.eq(r::id)))
.filter(dc::pushed_by.eq(uid).or(crate::has_permissions!(
uid,
r::id,
Perm::EDIT_DISCUSSION.bits()
)))
.filter(r::is_active)
.select((r::id, d::id, dc::change))
.get_result::<(uuid::Uuid, uuid::Uuid, String)>(&mut txn)
.await
.optional()?;
let (repo_id, disc_id, hash) = if let Some(disc_id) = repo_disc {
disc_id
} else {
return Ok(StatusCode::FORBIDDEN.into_response());
};
let hash = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();
if add {
can_add(&config, &mut txn, repo_id, disc_id, hash).await?;
diesel::update(d::discussions.find(disc_id))
.set(d::changes.eq(d::changes + 1))
.execute(&mut txn)
.await?;
} else {
can_remove(&config, &mut txn, repo_id, disc_id, hash).await?;
diesel::update(d::discussions.find(disc_id))
.set(d::changes.eq(d::changes - 1))
.execute(&mut txn)
.await?;
}
diesel::update(dc::discussion_changes.find(form.discussion_change))
.set(dc::removed.eq(if add { None } else { Some(Utc::now()) }))
.execute(&mut txn)
.await?;
Ok(Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, tree.disc
))
.into_response())
})
.scope_boxed()
})
.await
}
async fn can_add(
config: &Config,
txn: &mut AsyncPgConnection,
repo_id: uuid::Uuid,
disc_id: uuid::Uuid,
hash: libpijul::Hash,
) -> Result<(), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
let other_changes = dc::discussion_changes
.filter(dc::discussion.eq(disc_id))
.select((dc::change, dc::removed))
.get_results::<(String, Option<DateTime<Utc>>)>(txn)
.await?;
let repo_locks = config.repo_locks.clone();
let repo = repo_locks.get(&repo_id).await.unwrap();
let change =
tokio::task::spawn_blocking(move || repo.changes.get_change(&hash).unwrap()).await?;
for (o, removed) in other_changes {
if removed.is_some() {
let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
if change.dependencies.contains(&o) {
return Err(crate::Error::DependedUpon);
}
}
}
Ok(())
}
async fn can_remove(
config: &Config,
txn: &mut AsyncPgConnection,
repo_id: uuid::Uuid,
disc_id: uuid::Uuid,
hash: libpijul::Hash,
) -> Result<(), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
let other_changes = dc::discussion_changes
.filter(dc::discussion.eq(disc_id))
.select((dc::change, dc::removed))
.get_results::<(String, Option<DateTime<Utc>>)>(txn)
.await?;
let repo_locks = config.repo_locks.clone();
let repo = repo_locks.get(&repo_id).await.unwrap();
tokio::task::spawn_blocking(move || {
for (o, removed) in other_changes {
if removed.is_none() {
let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
let change = repo.changes.get_change(&o).unwrap();
if change.dependencies.contains(&hash) {
return Err(crate::Error::DependedUpon);
}
}
}
Ok(())
})
.await?
}
#[derive(Debug, Deserialize)]
pub struct NewCommentForm {
#[serde(default)]
edit_comment: Option<uuid::Uuid>,
new_discussion_comment: String,
uniq: i64,
token: String,
#[serde(default)]
action: Option<Action>,
}
#[debug_handler]
pub async fn discussion_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<DiscPath>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(form): Form<NewCommentForm>,
) -> Result<Response, crate::Error> {
debug!("discussion_post {:?}", form);
token.verify(&form.token)?;
debug!("new comment {:?}", tree);
let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let repo = crate::repository::repository(
&mut db,
&tree.owner,
&tree.repo,
uid,
Perm::CREATE_DISCUSSION,
)
.await?;
db.transaction(move |mut txn| {
(async move {
use crate::db::discussions::dsl as d;
let ip_sql =
ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
.unwrap();
let (disc_id, author): (uuid::Uuid, Option<uuid::Uuid>) = d::discussions
.filter(d::repository_id.eq(repo.id))
.filter(d::number.eq(tree.disc))
.select((d::id, d::author))
.get_result(&mut txn)
.await
.unwrap();
match form.action {
Some(Action::Close) | Some(Action::Reopen) => {
if author == Some(uid) || repo.permissions.contains(Perm::EDIT_DISCUSSION) {
use crate::db::discussion_tags::dsl as dt;
diesel::update(dt::discussion_tags)
.filter(dt::discussion.eq(disc_id))
.filter(dt::tag.is_null())
.set(dt::active.eq(false))
.execute(&mut txn)
.await?;
let n = diesel::insert_into(dt::discussion_tags)
.values((
dt::discussion.eq(disc_id),
dt::addition.eq(form.action == Some(Action::Reopen)),
dt::author.eq(uid),
))
.execute(&mut txn)
.await?;
if n > 0 {
if let Some(Action::Close) = form.action {
diesel::update(d::discussions.find(disc_id))
.set(d::closed.eq(diesel::dsl::now))
.execute(&mut txn)
.await?;
} else {
diesel::update(d::discussions.find(disc_id))
.set(d::closed.eq(None::<chrono::DateTime<chrono::Utc>>))
.execute(&mut txn)
.await?;
}
}
}
}
_ => {}
}
use crate::db::comments::dsl as c;
let comment = form.new_discussion_comment.trim();
if !comment.is_empty() {
let comment_html =
crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
if let Some(cid) = form.edit_comment {
diesel::update(c::comments.find(cid))
.filter(c::author.eq(uid))
.set((
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
))
.execute(&mut txn)
.await?;
} else {
diesel::insert_into(c::comments)
.values((
c::discussion_id.eq(disc_id),
c::author.eq(uid),
c::creation_ip.eq(&ip_sql),
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
c::uniq.eq(&form.uniq),
))
.execute(&mut txn)
.await?;
}
}
use crate::db::discussion_subscriptions::dsl as ds;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
.execute(&mut txn)
.await?;
if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
Ok::<_, crate::Error>(
(
token.clone(),
Json(
discussion_json(
&config,
&mut txn,
repo.id,
token,
Path(tree),
Some(login),
Some(email),
)
.await?,
),
)
.into_response(),
)
} else {
Ok::<_, crate::Error>(
Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, tree.disc
))
.into_response(),
)
}
})
.scope_boxed()
})
.await
}
pub async fn discussion_changelist(
db: &mut diesel_async::AsyncPgConnection,
changes: &crate::repository::changestore::FileSystem,
repo: uuid::Uuid,
disc: i32,
) -> Result<Vec<u8>, crate::Error> {
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
let mut n: usize = 0;
let mut m = libpijul::pristine::Merkle::zero();
let mut data = Vec::new();
use libpijul::pristine::Base32;
use std::io::Write;
for h in dc::discussion_changes
.inner_join(d::discussions)
.filter(d::repository_id.eq(repo))
.filter(d::number.eq(disc))
.filter(dc::removed.is_null())
.order_by(dc::added)
.select(dc::change)
.get_results::<String>(db)
.await?
{
let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
if changes.has_change(&hh) {
m = m.next(&hh);
writeln!(data, "{}.{}.{}", n, h, m.to_base32())?;
n += 1
}
}
data.push(b'\n');
Ok(data)
}
pub async fn discussion_state(
db: &mut diesel_async::AsyncPgConnection,
repo: uuid::Uuid,
disc: i32,
at: Option<u64>,
) -> Result<(usize, libpijul::pristine::Merkle), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
let q = dc::discussion_changes
.inner_join(d::discussions)
.filter(d::repository_id.eq(repo))
.filter(d::number.eq(disc))
.filter(dc::removed.is_null())
.order_by(dc::added)
.select(dc::change);
let it = if let Some(at) = at {
q.limit(at as i64 + 1).get_results::<String>(db).await?
/*
self.query_raw(
"SELECT change FROM discussion_changes JOIN discussions ON discussion_changes.discussion = discussions.id WHERE discussions.repository_id = $1 AND discussions.number = $2 AND removed IS NULL ORDER BY added ASC LIMIT $3",
crate::helpers::slice_iter(&[&repo, &disc, &(at as i64 + 1)])
).await?
*/
} else {
q.get_results::<String>(db).await?
/*
self.query_raw(
"SELECT change FROM discussion_changes JOIN discussions ON discussion_changes.discussion = discussions.id WHERE discussions.repository_id = $1 AND discussions.number = $2 AND removed IS NULL ORDER BY added ASC",
crate::helpers::slice_iter(&[&repo, &disc])
).await?
*/
};
let mut n = 0;
let mut m = libpijul::pristine::Merkle::zero();
use libpijul::pristine::Base32;
for h in it {
let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
m = m.next(&hh);
n += 1
}
Ok((n, m))
}
// @generated automatically by Diesel CLI.
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
comments (id) {
id -> Uuid,
discussion_id -> Uuid,
author -> Nullable<Uuid>,
contents -> Text,
creation_ip -> Inet,
creation_date -> Timestamptz,
cached_html -> Text,
uniq -> Int8,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
contributors (repo, key) {
repo -> Uuid,
key -> Bytea,
revision -> Timestamptz,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
discussion_changes (id) {
id -> Uuid,
change -> Text,
discussion -> Uuid,
pushed_by -> Nullable<Uuid>,
added -> Timestamptz,
removed -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
discussion_subscriptions (id) {
id -> Uuid,
user_id -> Uuid,
discussion_id -> Uuid,
start_date -> Nullable<Timestamptz>,
end_date -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
discussion_tags (id) {
id -> Uuid,
discussion -> Uuid,
tag -> Nullable<Uuid>,
author -> Nullable<Uuid>,
date -> Timestamptz,
active -> Bool,
addition -> Bool,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
discussions (id) {
id -> Uuid,
repository_id -> Uuid,
title -> Text,
author -> Nullable<Uuid>,
creation_ip -> Inet,
creation_date -> Timestamptz,
number -> Int4,
pull_to_branch -> Nullable<Text>,
first_patch -> Nullable<Int4>,
n_changes -> Int4,
changes -> Int4,
closed -> Nullable<Timestamptz>,
uniq -> Int8,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
email_log (time) {
time -> Timestamptz,
discussion -> Uuid,
email -> Text,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
hooks (id) {
id -> Uuid,
repository -> Uuid,
author -> Uuid,
creation_date -> Timestamptz,
active -> Bool,
secret -> Text,
url -> Text,
action -> Nullable<Int8>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
old_logins (login) {
login -> Citext,
user_id -> Nullable<Uuid>,
retired -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
password_failures (date) {
date -> Timestamptz,
ip -> Inet,
login -> Text,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
permissions (user_id, repo_id) {
user_id -> Uuid,
repo_id -> Uuid,
perm -> Int8,
start_date -> Timestamptz,
end_date -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
profile_pics (user_id) {
user_id -> Uuid,
updated -> Timestamptz,
format -> Text,
image -> Bytea,
small -> Bytea,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
publickeys (id) {
id -> Uuid,
user_id -> Uuid,
publickey -> Text,
bin -> Bytea,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
repositories (id) {
id -> Uuid,
owner -> Uuid,
name -> Text,
creation_ip -> Text,
fork_origin -> Nullable<Uuid>,
creation_date -> Timestamptz,
next_discussion_number -> Int4,
default_channel -> Text,
description -> Nullable<Text>,
is_active -> Bool,
max_changes_size -> Int8,
n_followers -> Int8,
last_pushed -> Timestamptz,
last_discussed -> Timestamptz,
rank -> Float8,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
repository_followers (repository_id, user_id) {
repository_id -> Uuid,
user_id -> Uuid,
start_date -> Nullable<Timestamptz>,
end_date -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
signingkeys (public_key) {
public_key -> Bytea,
user_id -> Uuid,
algorithm -> Keyalgorithm,
signature -> Bytea,
expires -> Nullable<Timestamptz>,
added -> Timestamptz,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
tags (id) {
id -> Uuid,
repository_id -> Uuid,
name -> Text,
creation_date -> Timestamptz,
deletion_date -> Nullable<Timestamptz>,
color -> Int4,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
tokens (token) {
token -> Bytea,
user_id -> Uuid,
date -> Timestamp,
}
}
diesel::table! {
use diesel::sql_types::*;
use diesel::pg::sql_types::*;
use crate::{Keyalgorithm};
users (id) {
id -> Uuid,
login -> Citext,
password -> Text,
email -> Citext,
email_is_invalid -> Nullable<Bool>,
creation_date -> Timestamptz,
is_active -> Bool,
name -> Nullable<Text>,
creation_ip -> Nullable<Inet>,
last_identity_change -> Nullable<Timestamptz>,
storage_used -> Nullable<Int8>,
suspended -> Bool,
}
}
diesel::joinable!(comments -> discussions (discussion_id));
diesel::joinable!(comments -> users (author));
diesel::joinable!(contributors -> repositories (repo));
diesel::joinable!(discussion_changes -> discussions (discussion));
diesel::joinable!(discussion_changes -> users (pushed_by));
diesel::joinable!(discussion_subscriptions -> discussions (discussion_id));
diesel::joinable!(discussion_subscriptions -> users (user_id));
diesel::joinable!(discussion_tags -> discussions (discussion));
diesel::joinable!(discussion_tags -> tags (tag));
diesel::joinable!(discussion_tags -> users (author));
diesel::joinable!(discussions -> repositories (repository_id));
diesel::joinable!(discussions -> users (author));
diesel::joinable!(email_log -> discussions (discussion));
diesel::joinable!(hooks -> repositories (repository));
diesel::joinable!(hooks -> users (author));
diesel::joinable!(permissions -> repositories (repo_id));
diesel::joinable!(permissions -> users (user_id));
diesel::joinable!(profile_pics -> users (user_id));
diesel::joinable!(publickeys -> users (user_id));
diesel::joinable!(repositories -> users (owner));
diesel::joinable!(repository_followers -> repositories (repository_id));
diesel::joinable!(repository_followers -> users (user_id));
diesel::joinable!(signingkeys -> users (user_id));
diesel::joinable!(tags -> repositories (repository_id));
diesel::joinable!(tokens -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
comments,
contributors,
discussion_changes,
discussion_subscriptions,
discussion_tags,
discussions,
email_log,
hooks,
old_logins,
password_failures,
permissions,
profile_pics,
publickeys,
repositories,
repository_followers,
signingkeys,
tags,
tokens,
users,
);
use serde_derive::*;
#[derive(Default, Serialize, Deserialize)]
pub struct EmailConfig {
pub source: String,
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct PostgresConfig {
pub host: String,
pub port: Option<u16>,
pub readonly_port: Option<u16>,
pub local_port: Option<u16>,
pub user: String,
pub password: String,
pub db: String,
pub log: Option<String>,
pub ca_file: Option<String>,
#[serde(default = "bool_true")]
pub use_tls: bool,
}
fn bool_true() -> bool {
true
}
#[derive(Default, Serialize, Deserialize)]
pub struct ConfigFile {
pub repository_cache_size: usize,
pub change_cache_size: usize,
pub lock_file: String,
pub max_body_length: u64,
pub hard_max_body_length: u64,
pub host: String,
pub max_password_attempts: i64,
pub http: HttpConfigFile,
pub email: EmailConfig,
pub postgres: Option<String>,
pub ssh: SshConfigFile,
pub workers: Option<usize>,
pub user: Option<String>,
pub group: Option<String>,
pub repositories_path: String,
pub time: TimeConfigFile,
pub failed_auth_timeout_millis: u64,
pub partial_change_size: u64,
pub prometheus: Prometheus,
#[serde(default)]
pub svelte_socket: Option<String>,
pub size_limit: Option<i64>,
pub version_time: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct HttpConfigFile {
pub http_port: u16,
pub https_port: u16,
pub ws_port: u16,
pub ws_timeout_secs: u64,
pub time_file: String,
pub log_file: String,
pub timeout_secs: u64,
pub perf_log: Option<String>,
pub connections_log: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct SshConfigFile {
pub port: u16,
pub timeout_secs: u64,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TimeConfigFile {
pub max_relative_days: usize,
pub yesterday_threshold_hours: usize,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Prometheus {
pub buckets_start: f64,
pub n_buckets: usize,
pub buckets_width: Option<f64>,
pub buckets_factor: Option<f64>,
}
use crate::config_file;
use sha2::Digest;
use rusoto_credential;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, SystemTime};
use thiserror::*;
use tracing::*;
use {thrussh, thrussh_keys};
pub struct Cached {
pub body: bytes::Bytes,
pub content_type: Option<hyper::header::HeaderValue>,
}
pub struct Config {
pub csrf: axum_csrf::CsrfConfig,
pub repository_cache_size: usize,
pub max_body_length: u64,
pub hard_max_body_length: u64,
pub host: String,
pub hostname: String,
pub http_timeout: std::time::Duration,
pub ssh: Arc<thrussh::server::Config>,
pub ssh_timeout: std::time::Duration,
pub hmac_secret: [u8; SHA512_OUTPUT_LEN],
pub repositories_path: PathBuf,
pub version_time: SystemTime,
pub version_time_str: String,
pub max_relative_days: usize,
pub yesterday_threshold_hours: usize,
pub failed_auth_timeout: Duration,
pub email: rusoto_credential::StaticProvider,
pub email_source: String,
pub cache: RwLock<HashMap<String, Cached>>,
pub cache_gzip: RwLock<HashMap<String, Cached>>,
pub cache_br: RwLock<HashMap<String, Cached>>,
pub db: Db,
// pub local_db: tokio_postgres::Client,
pub partial_change_size: u64,
pub ws_timeout: std::time::Duration,
pub change_cache: Arc<
std::sync::Mutex<
lru_cache::LruCache<
(uuid::Uuid, libpijul::ChangeId),
Arc<std::sync::Mutex<libpijul::change::ChangeFile>>,
>,
>,
>,
pub hash_cache: Arc<
std::sync::Mutex<
lru_cache::LruCache<
(uuid::Uuid, libpijul::Hash),
Arc<std::sync::Mutex<libpijul::change::ChangeFile>>,
>,
>,
>,
pub max_password_attempts: i64,
pub syntax_set: syntect::parsing::SyntaxSet,
pub svelte_socket: Option<String>,
pub render_cache: Mutex<lru_cache::LruCache<CachedItem, Resp>>,
pub size_limit: i64,
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub enum CachedItem {
Change(libpijul::Hash),
}
#[derive(Debug, Clone)]
pub struct Resp {
pub time: std::time::SystemTime,
pub body: Vec<u8>,
}
use clap::*;
#[derive(Debug, Parser)]
pub struct App {
#[arg(short, long)]
config: PathBuf,
#[arg(short, long)]
replication: PathBuf,
}
pub fn from_app() -> (config_file::ConfigFile, replication::ConfigFile) {
let matches = App::parse();
let config = toml::from_str(&std::fs::read_to_string(&matches.config).unwrap()).unwrap();
let repl = toml::from_str(&std::fs::read_to_string(&matches.replication).unwrap()).unwrap();
(config, repl)
}
pub fn drop_privileges(c: &config_file::ConfigFile) {
if let (&Some(ref user), &Some(ref group)) = (&c.user, &c.group) {
println!("Dropping privileges");
privdrop::PrivDrop::default()
.user(&user)
.group(&group)
.apply()
.unwrap();
}
}
#[derive(Debug, Error)]
pub enum TlsError {
#[error(transparent)]
OpenSSL(#[from] openssl::error::ErrorStack),
}
pub async fn make_tls() -> Result<axum_server::tls_rustls::RustlsConfig, TlsError> {
let key = std::env::var("tls_key").unwrap().into_bytes();
let certs = std::env::var("tls_cert").unwrap().into_bytes();
Ok(axum_server::tls_rustls::RustlsConfig::from_pem(certs, key)
.await
.unwrap())
}
const SHA512_OUTPUT_LEN: usize = 512 / 8;
pub type Db = diesel_async::pooled_connection::deadpool::Pool<diesel_async::AsyncPgConnection>;
pub async fn from_file(config_file: &config_file::ConfigFile) -> Config {
debug!("connecting to dbr");
let config = diesel_async::pooled_connection::AsyncDieselConnectionManager::<
diesel_async::AsyncPgConnection,
>::new(
config_file
.postgres
.as_deref()
.unwrap_or(&std::env::var("DATABASE_URL").unwrap()),
);
let db = diesel_async::pooled_connection::deadpool::Pool::builder(config)
.build()
.unwrap();
let ssh_keys = thrussh_keys::decode_secret_key(
&std::env::var("ssh_secret").expect("missing ssh_secret"),
None,
)
.unwrap();
let hmac_secret = {
let mut hasher = sha2::Sha512::new();
hasher.update(std::env::var("hmac_secret")
.expect("missing hmac_secret")
.as_bytes());
let mut out = [0; SHA512_OUTPUT_LEN];
out.clone_from_slice(&hasher.finalize());
out
};
let ssh_config = {
let mut config = thrussh::server::Config::default();
use thrussh::MethodSet;
config.methods =
MethodSet::PUBLICKEY | MethodSet::PASSWORD | MethodSet::KEYBOARD_INTERACTIVE;
config.keys.push(ssh_keys);
config.maximum_packet_size = 10_000_000;
config
};
Config {
csrf: axum_csrf::CsrfConfig::new(),
repository_cache_size: config_file.repository_cache_size,
max_body_length: config_file.max_body_length,
hard_max_body_length: config_file.hard_max_body_length,
http_timeout: std::time::Duration::from_secs(config_file.http.timeout_secs),
ws_timeout: std::time::Duration::from_secs(config_file.http.ws_timeout_secs),
email: rusoto_credential::StaticProvider::new(
std::env::var("aws_access_key_id").expect("missing aws_access_key_id"),
std::env::var("aws_access_key").expect("missing aws_access_key"),
None,
None,
),
email_source: config_file.email.source.clone(),
hostname: config_file.host.clone(),
host: {
let mut h = config_file.host.clone();
if config_file.http.https_port != 443 {
h.push_str(&format!(":{}", config_file.http.https_port));
}
h
},
ssh: Arc::new(ssh_config),
ssh_timeout: std::time::Duration::from_secs(config_file.ssh.timeout_secs),
hmac_secret,
cache: RwLock::new(HashMap::new()),
cache_br: RwLock::new(HashMap::new()),
cache_gzip: RwLock::new(HashMap::new()),
repositories_path: PathBuf::from(&config_file.repositories_path),
version_time: httpdate::parse_http_date(&config_file.version_time).unwrap(),
version_time_str: config_file.version_time.clone(),
max_relative_days: config_file.time.max_relative_days,
yesterday_threshold_hours: config_file.time.yesterday_threshold_hours,
failed_auth_timeout: Duration::from_millis(config_file.failed_auth_timeout_millis),
db,
partial_change_size: config_file.partial_change_size,
change_cache: Arc::new(std::sync::Mutex::new(lru_cache::LruCache::new(
config_file.change_cache_size,
))),
hash_cache: Arc::new(std::sync::Mutex::new(lru_cache::LruCache::new(
config_file.change_cache_size,
))),
max_password_attempts: config_file.max_password_attempts,
syntax_set: syntect::parsing::SyntaxSet::load_defaults_newlines(),
svelte_socket: config_file.svelte_socket.clone(),
render_cache: Mutex::new(lru_cache::LruCache::new(1024)),
size_limit: config_file.size_limit.unwrap_or(0),
}
}
pub struct AcceptJson(pub bool);
use http::HeaderValue;
impl headers::Header for AcceptJson {
fn name() -> &'static headers::HeaderName {
&http::header::ACCEPT
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
where
I: Iterator<Item = &'i HeaderValue>,
{
Ok(AcceptJson(
values
.filter_map(|x| x.to_str().ok())
.flat_map(|x| x.split(","))
.any(|x| x.split(";").next() == Some("application/json")),
))
}
fn encode<E>(&self, values: &mut E)
where
E: Extend<headers::HeaderValue>,
{
if self.0 {
values.extend(std::iter::once(headers::HeaderValue::from_static(
"application/json",
)))
}
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Color(pub i32);
impl Color {
pub fn to_string(&self) -> String {
format!(
"#{:02x}{:02x}{:02x}",
(self.0 >> 16) & 0xff,
(self.0 >> 8) & 0xff,
self.0 & 0xff
)
}
pub fn fg(&self) -> Color {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
let l = |c: f64| {
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055f64) / 1.055f64).powf(2.4)
}
};
let r: f64 = l((self.0 >> 16) as f64 / 255.);
let g = l(((self.0 >> 8) & 0xff) as f64 / 255.);
let b = l((self.0 & 0xff) as f64 / 255.);
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let black_contrast = (lum + 0.05) / 0.05;
let white_contrast = 1.1 / (lum + 0.05);
if white_contrast > black_contrast {
Color(0xffffff)
} else {
Color(0)
}
}
}
use crate::permissions::Perm;
use crate::repository::RepoPath;
use crate::Config;
use axum::{
extract::{Form, State},
response::{IntoResponse, Redirect, Response},
routing::post,
Router,
};
use axum_extra::extract::SignedCookieJar;
use serde_derive::*;
use tracing::*;
pub fn router() -> Router<Config> {
Router::new()
.route("/{owner}/{repo}/fork", post(fork))
.route("/{owner}/{repo}/prune", post(prune))
.route("/{owner}/{repo}/rename", post(rename))
.fallback(crate::fallback)
}
pub const MAIN_CHANNEL: &'static str = "main";
#[derive(Debug, Deserialize)]
pub struct Fork {
name: String,
token: String,
}
async fn fork(
State(config): State<crate::Config>,
jar: SignedCookieJar,
p: RepoPath,
token: axum_csrf::CsrfToken,
Form(fork): Form<Fork>,
) -> Result<Response, crate::Error> {
debug!("fork {:?}", fork);
token.verify(&fork.token)?;
let uid = crate::get_user_id(&jar)?;
let mut it = p.repo.split(":");
let repo = it.next().unwrap();
let channel = it.next().unwrap_or("");
let mut db = config.db.get().await?;
let (repo, _) = crate::repository::repository_id(&mut db, &p.owner, &repo, uid, Perm::EDIT_CHANNELS)
.await?;
let redirect = format!("/{}/{}:{}", p.owner, p.repo, fork.name);
debug!("fork id = {:?}", repo);
config
.replicator
.handle_update(
None,
None,
None,
::replication::Update::Fork {
repo,
channel: if channel.is_empty() {
MAIN_CHANNEL.to_string()
} else {
channel.to_string()
},
new: fork.name,
},
)
.await
.unwrap();
Ok(Redirect::to(&redirect).into_response())
}
async fn rename(
State(config): State<crate::Config>,
jar: SignedCookieJar,
p: RepoPath,
token: axum_csrf::CsrfToken,
Form(rename): Form<Fork>,
) -> Result<Response, crate::Error> {
debug!("rename {:?} {:?}", p, rename);
if p.channel.as_deref().unwrap_or(MAIN_CHANNEL) == MAIN_CHANNEL {
debug!("trying to rename main channel");
return Ok(Redirect::to(&format!("/{}/{}", p.owner, p.repo)).into_response())
}
token.verify(&rename.token)?;
let uid = crate::get_user_id(&jar)?;
let mut db = config.db.get().await?;
let (repo, _) =
crate::repository::repository_id(&mut db, &p.owner, &p.repo, uid, Perm::EDIT_CHANNELS)
.await?;
let redirect = format!("/{}/{}:{}", p.owner, p.repo, rename.name);
config
.replicator
.handle_update(
None,
None,
None,
::replication::Update::Rename {
repo,
channel: p.channel.unwrap_or_else(|| MAIN_CHANNEL.to_string()),
new: rename.name,
},
)
.await?;
Ok(Redirect::to(&redirect).into_response())
}
#[derive(Debug, Deserialize)]
pub struct Prune {
token: String,
}
async fn prune(
State(config): State<crate::Config>,
jar: SignedCookieJar,
p: RepoPath,
token: axum_csrf::CsrfToken,
Form(prune): Form<Prune>,
) -> Result<Response, crate::Error> {
debug!("prune {:?}", prune);
if p.channel.as_deref().unwrap_or(MAIN_CHANNEL) == MAIN_CHANNEL {
return Ok(Redirect::to(&format!("/{}/{}", p.owner, p.repo)).into_response())
}
token.verify(&prune.token)?;
let uid = crate::get_user_id(&jar)?;
let mut db = config.db.get().await.unwrap();
let (repo, _) =
crate::repository::repository_id(&mut db, &p.owner, &p.repo, uid, Perm::EDIT_CHANNELS)
.await?;
config
.replicator
.handle_update(
None,
None,
None,
::replication::Update::Prune {
repo,
channel: p.channel.unwrap_or_else(|| MAIN_CHANNEL.to_string()),
},
)
.await?;
Ok(Redirect::to(&format!("/{}/{}", p.owner, p.repo)).into_response())
}
use crate::permissions::Perm;
use crate::Config;
use axum::{
extract::State,
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Form, Router,
};
use axum_extra::extract::SignedCookieJar;
use diesel::{OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use serde_derive::*;
use tracing::*;
mod get;
mod list;
use diesel::ExpressionMethods;
use libpijul;
use libpijul::Base32;
pub fn router() -> Router<Config> {
Router::new()
.route("/{owner}/{repo}/list", get(list::changelist))
.route("/{owner}/{repo}/get/{hash}", get(get::change))
.route("/{owner}/{repo}/unrecord", post(unrecord))
.fallback(crate::fallback_json)
}
#[derive(Debug, Clone)]
pub struct ChangeAuthor {
pub login: String,
}
pub async fn change_author(
db: &crate::config::Db,
author: &libpijul::change::Author,
) -> Result<ChangeAuthor, crate::Error> {
debug!("change_author: {:?}", author);
use crate::db::users::dsl as u;
if let Some(key) = author.0.get("key") {
let keyb = bs58::decode(key.as_bytes()).into_vec();
if let Ok(keyb) = keyb {
use crate::db::signingkeys::dsl as sk;
if let Some(login) = u::users
.inner_join(sk::signingkeys)
.filter(sk::public_key.eq(keyb))
.filter(u::is_active)
.select(u::login)
.get_result(&mut db.get().await?)
.await
.optional()?
{
return Ok(ChangeAuthor {
login,
});
}
}
Ok(ChangeAuthor {
login: key.to_string(),
})
} else if let Some(name) = author.0.get("name") {
Ok(ChangeAuthor {
login: name.to_string(),
})
} else {
Ok(ChangeAuthor {
login: "?".to_string(),
})
}
}
pub async fn authors_string(
db: &crate::config::Db,
authors: &[libpijul::change::Author],
) -> Result<Vec<String>, crate::Error> {
let mut result = Vec::new();
for a in authors {
result.push(change_author(db, &a).await?.login)
}
Ok(result)
}
#[derive(Debug, Default, Serialize, Clone)]
pub struct Author {
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
key: String,
}
pub async fn get_authors(
db: &mut diesel_async::AsyncPgConnection,
authors: &mut [libpijul::change::Author],
) -> Result<Vec<Author>, diesel::result::Error> {
let mut auth = Vec::with_capacity(authors.len());
for a in authors.iter_mut() {
if let Some(key) = a.0.remove("key") {
use crate::db::signingkeys::dsl as k;
use crate::db::users::dsl as u;
if let Some((login, name)) = k::signingkeys
.find(&bs58::decode(&key).into_vec().unwrap())
.inner_join(u::users)
.select((u::login, u::name))
.get_result::<(String, Option<String>)>(db)
.await
.optional()?
{
auth.push(Author {
key,
login: Some(login),
name,
})
} else {
auth.push(Author {
key,
..Author::default()
})
}
}
}
Ok(auth)
}
#[derive(Debug, Deserialize)]
pub struct UnrecordForm {
token: String,
hash: String,
}
async fn unrecord(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
repo: crate::repository::RepoPath,
Form(form): Form<UnrecordForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
let uid = crate::get_user_id_strict(&jar)?;
let mut db = config.db.get().await.unwrap();
let (id, _) = crate::repository::repository_id(&mut db, &repo.owner, &repo.repo, Some(uid), Perm::APPLY)
.await?;
config
.replicator
.handle_update(
None,
None,
None,
::replication::Update::Unrecord {
repo: id,
channel: repo.channel.unwrap_or_else(|| "main".to_string()),
hash: if let Some(h) = libpijul::Hash::from_base32(form.hash.as_bytes()) {
h
} else {
return Err(crate::Error::HashParse { hash: form.hash });
},
deps: config.replicator.deps(id).await?,
},
)
.await?;
Ok(Redirect::to(&format!("/{}/{}/change", repo.owner, repo.repo)).into_response())
}
use crate::permissions::Perm;
use crate::repository::channel_spec_id;
use crate::Config;
use axum::{
debug_handler,
extract::{Query, State},
response::{IntoResponse, Response},
Json,
};
use axum_extra::extract::SignedCookieJar;
use diesel_async::AsyncPgConnection;
use libpijul::{
changestore::ChangeStore,
pristine::{Position, TxnErr},
Base32, ChangeId, ChannelTxnT, DepsTxnT, GraphTxnT, TxnT, TxnTExt,
};
use serde_derive::*;
use std::collections::HashSet;
use super::Author;
use tracing::*;
#[derive(Debug, Serialize)]
struct Changes {
hashes: Vec<Change>,
owner: String,
repo: String,
channel: String,
channels: Vec<String>,
token: String,
login: Option<String>,
}
#[derive(Debug, Default, Serialize)]
struct Change {
hash: String,
state: String,
unrecordable: bool,
pos: u64,
n: u64,
authors: Vec<Author>,
message: String,
date: chrono::DateTime<chrono::Utc>,
#[serde(skip)]
hash_: Option<libpijul::Hash>,
#[serde(skip)]
header:
Option<tokio::task::JoinHandle<libpijul::change::ChangeHeader_<libpijul::change::Author>>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListQuery {
from: Option<u64>,
to: Option<u64>,
count: Option<u64>,
rev: Option<bool>,
channel: Option<String>,
req_paths: Option<Vec<String>>,
}
const DEFAULT_PAGE_SIZE: u64 = 20;
#[debug_handler]
pub async fn changelist(
State(config): State<Config>,
jar: SignedCookieJar,
tree: crate::repository::RepoPath,
token: axum_csrf::CsrfToken,
page: Query<ListQuery>,
) -> Result<Response, crate::Error> {
let (uid, login) = crate::get_user_login_(&jar, &config).await?;
let mut db = config.db.get().await?;
let (id, _) =
crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
let repo_locks = config.repo_locks.clone();
let channel_ = tree
.channel
.as_deref()
.unwrap_or_else(|| libpijul::DEFAULT_CHANNEL)
.to_string();
debug!("channel_ {:?}", channel_);
let channel = channel_.clone();
let req_paths: Vec<String> = if let Some(ref p) = page.req_paths {
p.iter().map(|x| x.to_string()).collect()
} else {
Vec::new()
};
let count = page.count.unwrap_or(DEFAULT_PAGE_SIZE);
let reverse = page.rev.unwrap_or(true);
let mut hashes = list_changes(
&repo_locks,
id,
&channel,
&req_paths,
reverse,
page.from,
count,
)
.await?;
let repo_ = repo_locks.get(&id).await?;
for h in hashes.iter_mut() {
let changes = repo_.changes.clone();
let hh = h.hash_.unwrap();
h.header = Some(tokio::task::spawn_blocking(move || {
changes.get_header(&hh).unwrap()
}));
}
for h in hashes.iter_mut() {
fill(&mut db, h).await?
}
debug!("hashes {:#?}", hashes);
let token_ = token.authenticity_token()?;
Ok((
token,
Json(Changes {
hashes,
channel: channel_.to_string(),
channels: repo_.channels().await,
owner: tree.owner,
repo: tree.repo,
token: token_,
login,
}),
)
.into_response())
}
async fn fill(db: &mut AsyncPgConnection, change: &mut Change) -> Result<(), crate::Error> {
let mut header = change.header.take().unwrap().await?;
info!("fill {:#?}", header);
change.message = header.message;
change.date = header.timestamp;
change.authors = super::get_authors(db, &mut header.authors).await?;
Ok(())
}
async fn list_changes(
repo_locks: &crate::repository::RepositoryLocks,
id: uuid::Uuid,
channel: &str,
req_paths: &[String],
reverse: bool,
from: Option<u64>,
count: u64,
) -> Result<Vec<Change>, crate::Error> {
let mut v = Vec::new();
let txn = {
let repo_ = repo_locks.get(&id).await?;
let pristine = repo_.pristine.read().await;
pristine.txn_begin()?
};
let c = channel_spec_id(id, &channel);
let channel = if let Some(channel) = txn.load_channel(&c)? {
channel
} else if channel.is_empty() || channel == libpijul::DEFAULT_CHANNEL {
return Ok(v);
} else {
debug!("channel not found {:?}", c);
return Err(crate::Error::ChannelNotFound {
channel: channel.to_string(),
});
};
let mut paths = HashSet::new();
let repo_ = repo_locks.get(&id).await?;
for r in req_paths {
if let Ok((p, ambiguous)) = txn.follow_oldest_path(&repo_.changes, &channel, &r) {
let h: libpijul::Hash = txn.get_external(&p.change)?.unwrap().into();
v.push(Change {
hash: h.to_base32(),
hash_: Some(h),
pos: u64::from_le(p.pos.0 .0),
..Change::default()
});
if ambiguous {
return Err(crate::Error::ChangeNotFound);
}
paths.insert(p);
paths.extend(
libpijul::fs::iter_graph_descendants(&txn, txn.graph(&*channel.read()), p)?
.filter_map(|x| x.ok()),
);
} else {
return Err(crate::Error::ChangeNotFound);
}
}
debug!("paths = {:?}", paths);
let tags: Vec<u64> = txn
.iter_tags(txn.tags(&*channel.read()), from.unwrap_or(0))?
.map(|k| (*k.unwrap().0).into())
.collect();
let mut tagsi = 0;
let mut from_ = from.unwrap_or(0);
let mut is_first = true;
let mut n = 0;
if reverse {
let ch = channel.read();
if !is_first && from_ == 0 {
return Ok(v);
}
let mut it = txn.reverse_log(&ch, if from_ == 0 { None } else { Some(from_) })?;
while let Some(x) = it.next() {
let (a, b) = x?;
debug!("reverse {:?} {:?}", a, b);
if !is_first && a == from_ {
continue;
}
is_first = false;
from_ = a;
push_patch(
&txn,
&paths,
&tags,
&ch,
reverse,
&mut tagsi,
&mut v,
(a, b),
)?;
n += 1;
if (count > 0 && n > count) || n >= 32 {
return Ok(v);
}
}
} else {
let ch = channel.read();
let mut it = txn.log(&ch, from_)?;
while let Some(x) = it.next() {
let (a, b) = x?;
debug!("log {:?} {:?}", a, b);
if !is_first && a == from_ {
continue;
}
is_first = false;
from_ = a;
push_patch(
&txn,
&paths,
&tags,
&ch,
reverse,
&mut tagsi,
&mut v,
(a, b),
)?;
n += 1;
if (count > 0 && n > count) || n >= 32 {
return Ok(v);
}
}
}
Ok(v)
}
fn push_patch<
E: std::error::Error,
T: GraphTxnT<GraphError = E> + ChannelTxnT + DepsTxnT<DepsError = E>,
>(
txn: &T,
paths: &HashSet<Position<ChangeId>>,
tags: &[u64],
channel: &T::Channel,
reverse: bool,
tagsi: &mut usize,
v: &mut Vec<Change>,
(n, (h, m)): (
u64,
(
&libpijul::pristine::SerializedHash,
&libpijul::pristine::SerializedMerkle,
),
),
) -> Result<(), TxnErr<E>> {
let h_int = txn.get_internal(h)?.unwrap();
if paths.is_empty()
|| paths
.iter()
.any(|x| x.change == *h_int || txn.get_touched_files(x, Some(h_int)).unwrap().is_some())
{
let h: libpijul::Hash = h.into();
let m: libpijul::Merkle = m.into();
if tags.get(*tagsi) == Some(&n) {
v.push(Change {
hash: h.to_base32(),
hash_: Some(h),
state: m.to_base32(),
..Change::default()
});
*tagsi += 1
} else {
let unrecordable = if reverse {
let ch = txn.get_internal(&h.into())?.unwrap();
let mut un = true;
for x in txn.iter_revdep(&ch)? {
let (a, b) = x?;
if a > ch {
break;
} else if a < ch {
continue;
}
if txn.get_changeset(T::changes(txn, channel), b)?.is_some() {
un = false;
break;
}
}
un
} else {
false
};
v.push(Change {
hash: h.to_base32(),
hash_: Some(h),
state: m.to_base32(),
n,
unrecordable,
..Change::default()
});
}
}
Ok(())
}
use crate::permissions::Perm;
use crate::Config;
use axum::{
debug_handler,
extract::{Path, State},
response::{IntoResponse, Response},
Json,
};
use axum_extra::extract::SignedCookieJar;
use diesel_async::AsyncPgConnection;
use libpijul::Hash;
use libpijul::{changestore::ChangeStore, Base32, EdgeFlags, TxnT};
use serde_derive::*;
use std::collections::HashMap;
use tracing::*;
pub struct Record<'a, L> {
pub rec: &'a libpijul::change::Hunk<Option<Hash>, L>,
pub hashes: &'a mut HashMap<Hash, usize>,
pub hash: &'a libpijul::Hash,
pub changes: &'a crate::repository::changestore::FileSystem,
pub hunk: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TreePath {
hash: String,
}
#[debug_handler]
pub async fn change(
State(config): State<Config>,
jar: SignedCookieJar,
repo: crate::repository::RepoPath,
token: axum_csrf::CsrfToken,
Path(tree): Path<TreePath>,
) -> Result<Response, crate::Error> {
let (uid, login) = crate::get_user_login_(&jar, &config).await?;
let mut db = config.db.get().await.unwrap();
let (id, _) =
crate::repository::repository_id(&mut db, &repo.owner, &repo.repo, uid, Perm::READ).await?;
let repo_locks = config.repo_locks.clone();
let repo_ = repo_locks.get(&id).await.unwrap();
let pristine = repo_.pristine.read().await;
let txn = pristine.txn_begin().unwrap();
let r = render(
&mut db,
&txn,
&token,
&repo_.changes,
repo.owner,
repo.repo,
libpijul::Hash::from_base32(tree.hash.as_bytes()).unwrap(),
login,
)
.await?;
debug!("r = {:#?}", r);
Ok((token, Json(r)).into_response())
}
async fn render<T: TxnT>(
db: &mut AsyncPgConnection,
txn: &T,
token: &axum_csrf::CsrfToken,
changes: &crate::repository::changestore::FileSystem,
owner: String,
repo: String,
hash: libpijul::Hash,
login: Option<String>,
) -> Result<Patch, crate::Error> {
use libpijul::changestore::ChangeStore;
let mut header = changes.get_header(&hash).unwrap();
let hunks = changes.get_changes(&hash).unwrap();
let dependencies = changes.get_dependencies(&hash).unwrap();
let extra_known = changes.get_extra_known(&hash).unwrap();
let mut deps = Vec::new();
let mut hashes = HashMap::new();
hashes.insert(hash, 0);
hashes.insert(libpijul::Hash::None, 1);
for h in dependencies.iter() {
hashes.insert(*h, deps.len() + 2);
deps.push(h.to_base32())
}
use std::collections::hash_map::Entry;
let mut known = Vec::new();
for hash in extra_known.iter() {
if let Entry::Vacant(e) = hashes.entry(*hash) {
e.insert(deps.len() + 2);
known.push(hash.to_base32())
}
}
let mut body = Vec::with_capacity(hunks.len());
for (hunk, c) in hunks.iter().enumerate() {
body.push(
(Record {
rec: c,
hashes: &mut hashes,
hash: &hash,
changes: &changes,
hunk,
})
.render(txn),
);
}
let mut hashes_: Vec<_> = hashes.into_iter().collect();
hashes_.sort_by(|a, b| a.1.cmp(&b.1));
let hashes: Vec<_> = hashes_.into_iter().map(|x| x.0.to_base32()).collect();
Ok(Patch {
authors: super::get_authors(db, &mut header.authors).await.unwrap(),
header,
hash: hash.to_base32(),
owner,
repo,
token: token.authenticity_token()?,
deps: Deps {
hashes,
deps,
known,
},
hunks: body,
login,
})
}
#[derive(Debug, Serialize, Clone)]
pub struct Metadata {
basename: String,
metadata: libpijul::pristine::InodeMetadata,
}
impl<'a> From<libpijul::changestore::FileMetadata<'a>> for Metadata {
fn from(f: libpijul::changestore::FileMetadata<'a>) -> Metadata {
Metadata {
basename: f.basename.to_string(),
metadata: f.metadata,
}
}
}
#[derive(Debug, Serialize, Clone)]
pub enum Contents {
Add(String),
Del(String),
}
type EdgeMap = libpijul::change::EdgeMap<usize>;
type NewVertex = libpijul::change::NewVertex<usize>;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
struct Pos {
hunk: usize,
sub: usize,
l1: usize,
}
pub type Position = libpijul::pristine::Position<usize>;
#[derive(Debug, Serialize, Clone)]
pub struct Inode(Position);
pub type Atom = libpijul::change::Atom<usize>;
#[derive(Debug, Serialize, Clone)]
pub enum Hunk {
FileAdd {
meta: Metadata,
context: Vec<Position>,
contents: Vec<Contents>,
},
FileMove {
meta: Metadata,
old: Vec<Metadata>,
up: Vec<Position>,
down: Vec<Position>,
show_perms: bool,
},
FileDel {
deleted_names: Vec<Metadata>,
del: Atom,
content_edges: Option<EdgeMap>,
content: Vec<Contents>,
},
FileUndel {
undel_names: Vec<Metadata>,
undel: Atom,
content_edges: Option<EdgeMap>,
content: Vec<Contents>,
},
SolveNameConflict {
old: Vec<Metadata>,
name: Atom,
},
UnsolveNameConflict {
old: Vec<Metadata>,
name: Atom,
},
Edit {
inode: Inode,
path: String,
line: usize,
atom: Atom,
contents: Vec<Contents>,
},
Replacement {
inode: Inode,
path: String,
line: usize,
ins: NewVertex,
del: EdgeMap,
ins_contents: Vec<Contents>,
del_contents: Vec<Contents>,
},
SolveOrderConflict {
inode: Inode,
path: String,
line: usize,
atom: Atom,
contents: Vec<Contents>,
},
UnsolveOrderConflict {
inode: Inode,
path: String,
line: usize,
atom: Atom,
contents: Vec<Contents>,
},
ResurrectZombie {
inode: Inode,
path: String,
line: usize,
atom: Atom,
contents: Vec<Contents>,
},
AddRoot {
inode: Inode,
atom: Atom,
name: Atom,
},
DelRoot {
inode: Inode,
atom: Atom,
name: Atom,
},
}
impl<'a> Record<'a, libpijul::change::Local> {
pub fn render<T: TxnT>(&'a mut self, txn: &T) -> Hunk {
match self.rec {
libpijul::change::Hunk::FileAdd {
add_name: libpijul::change::Atom::NewVertex(ref n),
ref contents,
..
} => self.render_file_add(txn, n, contents),
libpijul::change::Hunk::FileAdd { .. } => unreachable!(),
libpijul::change::Hunk::FileMove {
add: libpijul::change::Atom::NewVertex(ref add),
del,
..
} => self.render_file_move(txn, add, &del),
libpijul::change::Hunk::FileMove { .. } => unreachable!(),
libpijul::change::Hunk::FileDel { del, contents, .. } => {
self.render_file_del(txn, del, contents)
}
libpijul::change::Hunk::FileUndel {
undel, contents, ..
} => self.render_file_undel(txn, undel, contents),
libpijul::change::Hunk::SolveNameConflict { name, .. } => {
self.render_solve_name_conflict(txn, name)
}
libpijul::change::Hunk::UnsolveNameConflict { name, .. } => {
self.render_unsolve_name_conflict(txn, name)
}
libpijul::change::Hunk::Edit { change, local, .. } => {
self.render_edit(txn, change, local)
}
libpijul::change::Hunk::Replacement {
change: libpijul::change::Atom::EdgeMap(ref change),
replacement: libpijul::change::Atom::NewVertex(ref replacement),
local,
..
} => self.render_replacement(txn, change, replacement, local),
libpijul::change::Hunk::Replacement { .. } => unreachable!(),
libpijul::change::Hunk::SolveOrderConflict { change, local } => {
self.render_solve_order_conflict(txn, change, local)
}
libpijul::change::Hunk::UnsolveOrderConflict { change, local } => {
self.render_unsolve_order_conflict(txn, change, local)
}
libpijul::change::Hunk::ResurrectZombies { change, local, .. } => {
self.render_resurrect_zombies(txn, change, local)
}
libpijul::change::Hunk::AddRoot { inode, name } => {
self.render_add_root(txn, inode, name)
}
libpijul::change::Hunk::DelRoot { inode, name } => {
self.render_del_root(txn, inode, name)
}
}
}
}
impl<'a, L> Record<'a, L> {
fn inode<T: TxnT>(&mut self, txn: &T, id: &libpijul::change::Atom<Option<Hash>>) -> Inode {
Inode(position(txn, self.hashes, &id.inode()))
}
fn inode_edgemap<T: TxnT>(
&mut self,
txn: &T,
id: &libpijul::change::EdgeMap<Option<Hash>>,
) -> Inode {
Inode(position(txn, self.hashes, &id.inode))
}
fn hash(&mut self, hash: &Option<Hash>) -> usize {
if let Some(h) = hash {
let len = self.hashes.len();
*self.hashes.entry(h.clone()).or_insert(len + 1)
} else {
0
}
}
fn map_pos(
&mut self,
pos: &libpijul::pristine::Position<Option<Hash>>,
) -> libpijul::pristine::Position<usize> {
libpijul::pristine::Position {
change: self.hash(&pos.change),
pos: pos.pos,
}
}
fn map_vertex(&mut self, pos: &libpijul::Vertex<Option<Hash>>) -> libpijul::Vertex<usize> {
libpijul::Vertex {
change: self.hash(&pos.change),
start: pos.start,
end: pos.end,
}
}
fn atom(
&mut self,
atom: &libpijul::change::Atom<Option<Hash>>,
) -> libpijul::change::Atom<usize> {
use libpijul::change::Atom;
match atom {
Atom::NewVertex(v) => Atom::NewVertex(self.new_vertex(v)),
Atom::EdgeMap(e) => Atom::EdgeMap(self.edge_map(e)),
}
}
fn edge_map(
&mut self,
e: &libpijul::change::EdgeMap<Option<Hash>>,
) -> libpijul::change::EdgeMap<usize> {
libpijul::change::EdgeMap {
edges: e
.edges
.iter()
.map(|e| libpijul::change::NewEdge {
flag: e.flag,
previous: e.previous,
from: self.map_pos(&e.from),
to: self.map_vertex(&e.to),
introduced_by: self.hash(&e.introduced_by),
})
.collect(),
inode: self.map_pos(&e.inode),
}
}
fn new_vertex(
&mut self,
v: &libpijul::change::NewVertex<Option<Hash>>,
) -> libpijul::change::NewVertex<usize> {
libpijul::change::NewVertex {
up_context: v.up_context.iter().map(|x| self.map_pos(x)).collect(),
down_context: v.down_context.iter().map(|x| self.map_pos(x)).collect(),
start: v.start,
end: v.end,
flag: v.flag,
inode: self.map_pos(&v.inode),
}
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Patch {
header: libpijul::change::ChangeHeader,
hash: String,
authors: Vec<super::Author>,
deps: Deps,
hunks: Vec<Hunk>,
owner: String,
repo: String,
token: String,
login: Option<String>,
}
#[derive(Debug, Serialize, Clone)]
struct Deps {
hashes: Vec<String>,
deps: Vec<String>,
known: Vec<String>,
}
impl<'a> Record<'a, libpijul::change::Local> {
fn render_contents<T: TxnT>(
&self,
txn: &T,
v: &libpijul::change::Atom<Option<Hash>>,
lang: Option<&str>,
pos: Pos,
) -> Vec<Contents> {
match v {
libpijul::change::Atom::NewVertex(ref n) => {
self.render_newvertex_contents(txn, n, lang, pos)
}
libpijul::change::Atom::EdgeMap(ref e) => {
self.render_edgemap_contents(txn, e, lang, pos)
}
}
}
fn render_newvertex_contents<H, T: TxnT>(
&self,
_txn: &T,
v: &libpijul::change::NewVertex<H>,
_lang: Option<&str>,
_pos: Pos,
) -> Vec<Contents> {
let v_start: u64 = v.start.0.into();
let v_end: u64 = v.end.0.into();
let mut b = vec![0; v_end as usize - v_start as usize];
self.changes
.get_contents_ext(
libpijul::pristine::Vertex {
change: Some(*self.hash),
start: v.start,
end: v.end,
},
&mut b,
)
.unwrap();
let mut v = Vec::new();
if let Ok(l) = std::str::from_utf8(&b) {
for l in l.lines() {
v.push(Contents::Add(l.to_string()))
}
}
v
}
fn render_edgemap_contents<T: TxnT>(
&self,
_txn: &T,
contents: &libpijul::change::EdgeMap<Option<Hash>>,
_lang: Option<&str>,
_pos: Pos,
) -> Vec<Contents> {
let mut buf = Vec::new();
let mut current_edge = None;
let mut v = Vec::new();
for e in contents.edges.iter() {
if Some(e.to) == current_edge {
continue;
}
current_edge = Some(e.to);
buf.resize(e.to.end - e.to.start, 0);
if self.changes.get_contents_ext(e.to, &mut buf).is_err() {
continue;
}
let buf = if let Ok(buf) = std::str::from_utf8(&buf) {
buf
} else {
continue;
};
v.push(if e.flag.contains(EdgeFlags::DELETED) {
Contents::Del(buf.to_string())
} else {
Contents::Add(buf.to_string())
});
}
v
}
fn render_file_add<T: TxnT>(
&mut self,
txn: &T,
n: &libpijul::change::NewVertex<Option<libpijul::pristine::Hash>>,
contents: &Option<libpijul::change::Atom<Option<libpijul::pristine::Hash>>>,
) -> Hunk {
let n_start: u64 = n.start.0.into();
let n_end: u64 = n.end.0.into();
let mut buf = vec![0; (n_end - n_start) as usize];
self.changes
.get_contents_ext(
libpijul::pristine::Vertex {
change: Some(*self.hash),
start: n.start,
end: n.end,
},
&mut buf,
)
.unwrap();
let meta: Metadata = libpijul::changestore::FileMetadata::read(&buf).into();
let context = render_context(txn, self.hashes, &n.up_context[..]);
let contents = if let Some(ref contents) = contents {
let lang = if let Some(p) = meta.basename.rfind('.') {
Some(meta.basename.split_at(p + 1).1)
} else {
None
};
let pos = Pos {
hunk: self.hunk,
sub: 1,
l1: 0,
};
self.render_contents(txn, contents, lang, pos)
} else {
Vec::new()
};
Hunk::FileAdd {
meta,
context,
contents,
}
}
fn render_file_move<T: TxnT>(
&'a mut self,
txn: &T,
add: &'a libpijul::change::NewVertex<Option<libpijul::pristine::Hash>>,
del: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
) -> Hunk {
let add_start: u64 = add.start.0.into();
let add_end: u64 = add.end.0.into();
let mut buf = vec![0; (add_end - add_start) as usize];
self.changes
.get_contents_ext(
libpijul::pristine::Vertex {
change: Some(*self.hash),
start: add.start,
end: add.end,
},
&mut buf,
)
.unwrap();
let meta: Metadata = libpijul::changestore::FileMetadata::read(&buf).into();
let (old, show_perms) = if let libpijul::change::Atom::EdgeMap(ref del) = del {
self.render_deleted_names(txn, del, Some(meta.metadata))
} else {
(Vec::new(), true)
};
Hunk::FileMove {
meta,
old,
show_perms,
up: render_context(txn, self.hashes, &add.up_context[..]),
down: render_context(txn, self.hashes, &add.down_context[..]),
}
}
fn render_file_del<T: TxnT>(
&'a mut self,
txn: &T,
del: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
contents: &'a Option<libpijul::change::Atom<Option<libpijul::pristine::Hash>>>,
) -> Hunk {
let (deleted_names, _) = if let libpijul::change::Atom::EdgeMap(ref del) = del {
self.render_deleted_names(txn, del, None)
} else {
(Vec::new(), true)
};
Hunk::FileDel {
deleted_names,
del: self.atom(del),
content_edges: if let Some(libpijul::change::Atom::EdgeMap(ref contents)) = contents {
Some(self.edge_map(contents))
} else {
None
},
content: if let Some(libpijul::change::Atom::EdgeMap(ref contents)) = contents {
self.render_edgemap_contents(
txn,
contents,
None,
Pos {
hunk: self.hunk,
sub: 1,
l1: 0,
},
)
} else {
Vec::new()
},
}
}
fn render_file_undel<T: TxnT>(
&'a mut self,
txn: &T,
undel: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
contents: &'a Option<libpijul::change::Atom<Option<libpijul::pristine::Hash>>>,
) -> Hunk {
let (undel_names, _) = if let libpijul::change::Atom::EdgeMap(ref undel) = undel {
self.render_deleted_names(txn, undel, None)
} else {
(Vec::new(), true)
};
Hunk::FileUndel {
undel_names,
undel: self.atom(undel),
content_edges: if let Some(libpijul::change::Atom::EdgeMap(ref contents)) = contents {
Some(self.edge_map(contents))
} else {
None
},
content: if let Some(libpijul::change::Atom::EdgeMap(ref contents)) = contents {
self.render_edgemap_contents(
txn,
contents,
None,
Pos {
hunk: self.hunk,
sub: 0,
l1: 0,
},
)
} else {
Vec::new()
},
}
}
fn render_solve_name_conflict<T: TxnT>(
&'a mut self,
txn: &T,
name: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
) -> Hunk {
let (old, _) = if let libpijul::change::Atom::EdgeMap(ref name) = name {
self.render_deleted_names(txn, name, None)
} else {
(Vec::new(), true)
};
Hunk::SolveNameConflict {
old,
name: self.atom(name),
}
}
fn render_unsolve_name_conflict<T: TxnT>(
&'a mut self,
txn: &T,
name: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
) -> Hunk {
let (old, _) = if let libpijul::change::Atom::EdgeMap(ref name) = name {
self.render_deleted_names(txn, name, None)
} else {
(Vec::new(), true)
};
Hunk::UnsolveNameConflict {
old,
name: self.atom(name),
}
}
fn render_edit<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
local: &'a libpijul::change::Local,
) -> Hunk {
Hunk::Edit {
inode: self.inode(txn, change),
path: local.path.to_string(),
line: local.line,
atom: self.atom(&change),
contents: self.render_contents(
txn,
change,
None,
Pos {
hunk: self.hunk,
sub: 1,
l1: 0,
},
),
}
}
fn render_replacement<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::EdgeMap<Option<libpijul::pristine::Hash>>,
replacement: &'a libpijul::change::NewVertex<Option<libpijul::pristine::Hash>>,
local: &'a libpijul::change::Local,
) -> Hunk {
Hunk::Replacement {
inode: self.inode_edgemap(txn, change),
path: local.path.to_string(),
line: local.line,
del: self.edge_map(change),
del_contents: self.render_edgemap_contents(
txn,
change,
None,
Pos {
hunk: self.hunk,
sub: 0,
l1: 0,
},
),
ins: self.new_vertex(replacement),
ins_contents: self.render_newvertex_contents(
txn,
replacement,
None,
Pos {
hunk: self.hunk,
sub: 1,
l1: 0,
},
),
}
}
fn render_solve_order_conflict<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
local: &'a libpijul::change::Local,
) -> Hunk {
Hunk::SolveOrderConflict {
inode: self.inode(txn, change),
path: local.path.to_string(),
line: local.line,
atom: self.atom(change),
contents: self.render_contents(
txn,
change,
None,
Pos {
hunk: self.hunk,
sub: 0,
l1: 0,
},
),
}
}
fn render_unsolve_order_conflict<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
local: &'a libpijul::change::Local,
) -> Hunk {
Hunk::UnsolveOrderConflict {
inode: self.inode(txn, change),
path: local.path.to_string(),
line: local.line,
atom: self.atom(change),
contents: self.render_contents(
txn,
change,
None,
Pos {
hunk: self.hunk,
sub: 0,
l1: 0,
},
),
}
}
fn render_resurrect_zombies<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
local: &'a libpijul::change::Local,
) -> Hunk {
Hunk::ResurrectZombie {
inode: self.inode(txn, change),
path: local.path.to_string(),
line: local.line,
atom: self.atom(change),
contents: self.render_contents(
txn,
change,
None,
Pos {
hunk: self.hunk,
sub: 0,
l1: 0,
},
),
}
}
fn render_add_root<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
name: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
) -> Hunk {
Hunk::AddRoot {
inode: self.inode(txn, change),
atom: self.atom(change),
name: self.atom(name),
}
}
fn render_del_root<T: TxnT>(
&'a mut self,
txn: &T,
change: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
name: &'a libpijul::change::Atom<Option<libpijul::pristine::Hash>>,
) -> Hunk {
Hunk::DelRoot {
inode: self.inode(txn, change),
atom: self.atom(change),
name: self.atom(name),
}
}
fn render_deleted_names<T: TxnT>(
&self,
_txn: &T,
del: &libpijul::change::EdgeMap<Option<Hash>>,
perm: Option<libpijul::pristine::InodeMetadata>,
) -> (Vec<Metadata>, bool) {
let mut buf = Vec::new();
let mut show_perms = false;
let mut result = Vec::new();
for d in del.edges.iter() {
buf.resize(d.to.end - d.to.start, 0);
if self.changes.get_contents_ext(d.to, &mut buf).is_err() {
continue;
}
if !buf.is_empty() {
let meta: Metadata = libpijul::changestore::FileMetadata::read(&buf).into();
if let Some(perm) = perm {
if meta.metadata != perm {
show_perms = true;
}
}
result.push(meta);
}
}
(result, show_perms)
}
}
fn position<T: TxnT>(
_txn: &T,
hashes: &mut HashMap<Hash, usize>,
c: &libpijul::pristine::Position<Option<Hash>>,
) -> Position {
let n = if let Some(ref h) = c.change {
let len = hashes.len();
*hashes.entry(h.clone()).or_insert(len + 1)
} else {
0
};
Position {
change: n,
pos: c.pos,
}
}
fn render_context<T: TxnT>(
txn: &T,
hashes: &mut HashMap<Hash, usize>,
context: &[libpijul::pristine::Position<Option<Hash>>],
) -> Vec<Position> {
context.iter().map(|c| position(txn, hashes, c)).collect()
}
use crate::db;
use axum::{
debug_handler,
extract::{FromRef, Query, State},
response::{IntoResponse, Redirect, Response},
Form,
};
use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar};
use cuach::*;
use diesel::{
sql_types::Bool, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::*;
use rand::{rng, Rng};
use serde::*;
use thiserror::*;
use tracing::*;
impl FromRef<crate::Config> for Key {
fn from_ref(state: &crate::Config) -> Self {
Key::from(&state.hmac_secret)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginPayload {
login: String,
pass: String,
redirect: Option<String>,
}
#[macro_export]
macro_rules! check_password {
( $p:expr ) => {{
use diesel::sql_types::Text;
diesel::dsl::sql::<Bool>("password = crypt(")
.bind::<Text, _>($p)
.sql(", password)")
}};
}
#[macro_export]
macro_rules! make_password {
( $p:expr ) => {{
use diesel::sql_types::Text;
diesel::dsl::sql::<Text>("crypt(")
.bind::<Text, _>($p)
.sql(", gen_salt('bf'))")
}};
}
fn make_cookie(id: uuid::Uuid) -> Cookie<'static> {
let mut c = Cookie::new(
"session_id",
data_encoding::BASE64URL.encode(&bincode::serialize(&(id, chrono::Utc::now())).unwrap()),
);
c.set_path("/");
c
}
#[debug_handler]
pub async fn login(
State(config): State<crate::Config>,
jar: SignedCookieJar,
Form(payload): Form<LoginPayload>,
) -> Response {
debug!("{:?}", payload);
use db::users::dsl as u;
if let Some((id, login)) = u::users
.filter(u::email.eq(&payload.login).or(u::login.eq(&payload.login)))
.filter(u::email_is_invalid.is_null())
.filter(check_password!(&payload.pass))
.select((u::id, u::login))
.get_result::<(uuid::Uuid, String)>(&mut config.db.get().await.unwrap())
.await
.optional()
.unwrap()
{
debug!("id {:?} {:?}", id, login);
(
jar.add(make_cookie(id)),
Redirect::to(&payload.redirect.unwrap_or(format!("/{}", login))),
)
.into_response()
} else {
debug!("login not found");
Redirect::to(payload.redirect.as_deref().unwrap_or("/")).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterPayload {
login: String,
email: String,
pass: String,
confpass: String,
}
#[derive(Debug, Deserialize)]
pub struct RegisterToken {
token: String,
}
#[debug_handler]
pub async fn register_post(
State(config): State<crate::Config>,
Form(payload): Form<RegisterPayload>,
) -> Redirect {
debug!("{:?}", payload);
use db::users::dsl as u;
let mut db = config.db.get().await.unwrap();
if let Some(id) = diesel::insert_into(u::users)
.values((
u::login.eq(&payload.login),
u::email.eq(&payload.email),
u::password.eq(make_password!(payload.pass)),
u::email_is_invalid.eq(true),
))
.on_conflict_do_nothing()
.returning(u::id)
.get_result::<uuid::Uuid>(&mut db)
.await
.optional()
.unwrap()
{
if let Err(e) = make_email(&config, &mut db, id, true, payload.email).await {
debug!("{:?}", e);
diesel::delete(u::users.find(id))
.execute(&mut db)
.await
.unwrap();
} else {
return Redirect::to("/signup_mail_sent");
}
}
Redirect::to("/?error=alreadyExists")
}
#[debug_handler]
pub async fn register_get(
State(config): State<crate::Config>,
Query(params): Query<RegisterToken>,
jar: SignedCookieJar,
) -> Response {
use db::tokens::dsl as tokens;
use db::users::dsl as u;
let mut db = config.db.get().await.unwrap();
debug!("register_get {:?}", params);
if let Ok(token) = data_encoding::BASE64URL.decode(params.token.as_bytes()) {
debug!("token {:?}", token);
if let Some(id) = diesel::delete(tokens::tokens.find(&token))
.returning(tokens::user_id)
.get_result::<uuid::Uuid>(&mut db)
.await
.optional()
.unwrap()
{
diesel::update(u::users.find(id))
.set(u::email_is_invalid.eq(None as Option<bool>))
.execute(&mut db)
.await
.unwrap();
debug!("id {:?}", id);
if let Some(login) = u::users
.find(id)
.select(u::login)
.get_result::<String>(&mut db)
.await
.optional()
.unwrap()
{
return (
jar.add(make_cookie(id)),
Redirect::to(&format!("/{}", login)),
)
.into_response();
}
}
}
Redirect::to("/").into_response()
}
#[derive(Debug, Deserialize)]
pub struct Recover {
code: String,
pass: String,
pass2: String,
}
#[debug_handler]
pub async fn recover_reset(
State(config): State<crate::Config>,
jar: SignedCookieJar,
Form(recover): Form<Recover>,
) -> Result<Response, crate::Error> {
debug!("recover {:?}", recover);
if recover.pass != recover.pass2 {
return Ok(Redirect::to("/").into_response());
}
use db::tokens::dsl as tokens;
use db::users::dsl as u;
let mut db = config.db.get().await?;
if let Ok(token) = data_encoding::BASE64URL.decode(recover.code.as_bytes()) {
db.transaction(move |mut txn| {
(async move {
debug!("token {:?}", token);
if let Some(id) = diesel::delete(tokens::tokens.find(&token))
.returning(tokens::user_id)
.get_result::<uuid::Uuid>(&mut txn)
.await
.optional()?
{
debug!("id {:?}", id);
let n = diesel::update(u::users.find(id))
.set(u::password.eq(make_password!(recover.pass)))
.filter(u::email_is_invalid.is_null())
.returning(u::login)
.execute(&mut txn)
.await?;
if n > 0 {
return Ok((
jar.add(make_cookie(id)),
Redirect::to("/settings"),
)
.into_response());
} else {
debug!("no login");
}
}
Ok(Redirect::to("/").into_response())
})
.scope_boxed()
})
.await
} else {
debug!("no token decode");
Ok(Redirect::to("/").into_response())
}
}
#[derive(Debug, Deserialize)]
pub struct RecoverInit {
login: String,
}
#[debug_handler]
pub async fn recover_init(
State(config): State<crate::Config>,
Form(recover): Form<RecoverInit>,
) -> Result<Redirect, crate::Error> {
use db::users::dsl as u;
let mut db = config.db.get().await.unwrap();
if let Some((id, email)) = u::users
.filter(u::login.eq(&recover.login).or(u::email.eq(&recover.login)))
.select((u::id, u::email))
.get_result::<(uuid::Uuid, String)>(&mut db)
.await
.optional()?
{
make_email(&config, &mut db, id, false, email).await?;
Ok(Redirect::to("/recover/email_sent"))
} else {
Ok(Redirect::to("/recover"))
}
}
#[derive(Error, Debug, Clone, Copy)]
pub enum Error {
#[error("This user does not exist, or the email address used for registration was different.")]
NoSuchUser,
#[error("This user is inactive. Please contact <a href=\"mailto:support@pijul.org\">support@pijul.org</a> to resolve this situation.")]
Inactive,
#[error("The email could not be sent. Please check the address, and try again later.")]
Email,
#[error("Login already taken")]
LoginAlreadyTaken,
#[error("The login contains invalid characters")]
InvalidLogin,
#[error("The login is empty")]
EmptyLogin,
#[error("The two passwords do not match")]
PasswordMatchError,
#[error("Signup failed")]
CouldNotSignup,
}
pub const CONTENT_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("padding:1em;max-width:600px;margin:0 auto;");
pub const FOOTER_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("max-width:600px;margin:10px auto;font-size: small; color: #666666;");
pub const A_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("color:#007bff;text-decoration:none;");
/*
pub const EMAIL_CHARSET: &'static str = "UTF-8";
pub const BLOCKQUOTE_STYLE: cuach::PreEscaped<&'static str> =
cuach::PreEscaped("border-left:2px solid #666666;padding-left:10px;margin:30px 0 30px 30px;");
*/
impl std::str::FromStr for Error {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"NoSuchUser" => Ok(Error::NoSuchUser),
"Inactive" => Ok(Error::Inactive),
"Email" => Ok(Error::Email),
"LoginAlreadyTaken" => Ok(Error::LoginAlreadyTaken),
"InvalidLogin" => Ok(Error::InvalidLogin),
"EmptyLogin" => Ok(Error::EmptyLogin),
"PasswordMatchError" => Ok(Error::PasswordMatchError),
"CouldNotSignup" => Ok(Error::CouldNotSignup),
_ => Err(()),
}
}
}
impl cuach::Render for Error {
type Error = std::fmt::Error;
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), Self::Error> {
Ok(write!(w, "{}", self)?)
}
}
#[template(path = "email/signup.html")]
struct EmailSignupTemplate<'a> {
host: &'a str,
token: &'a str,
}
#[template(path = "email/password_reset.html")]
struct EmailPasswordResetTemplate<'a> {
host: &'a str,
// login: &'a str,
token: &'a str,
}
async fn make_email(
config: &crate::Config,
db: &mut AsyncPgConnection,
id: uuid::Uuid,
is_signup: bool,
address: String,
) -> Result<rusoto_ses::SendEmailResponse, crate::Error> {
let mut token = [0u8; 32];
rng().fill(&mut token[..]);
use crate::db::tokens::dsl as tokens;
diesel::insert_into(tokens::tokens)
.values((tokens::user_id.eq(id), tokens::token.eq(&token)))
.execute(db)
.await?;
debug!(
"token = {:?} {:?}",
token,
data_encoding::HEXLOWER.encode(&token)
);
let token = data_encoding::BASE64URL.encode(&token);
let subject = (if is_signup {
"Account confirmation"
} else {
"Password reset"
})
.to_string();
let mut body_html = String::new();
let charset = "UTF-8".to_string();
let body = if is_signup {
(EmailSignupTemplate {
host: &config.host,
token: &token,
})
.render_into(&mut body_html)
.unwrap();
rusoto_ses::Body {
text: Some(rusoto_ses::Content {
data: format!(
"Welcome to the nest.
You are just one click away from getting an account, click on the following link:
https://{}/register?token={}
",
config.host, token
),
charset: Some(charset.clone()),
}),
html: Some(rusoto_ses::Content {
data: body_html,
charset: Some(charset),
}),
}
} else {
(EmailPasswordResetTemplate {
host: &config.host,
token: &token,
// login,
})
.render_into(&mut body_html)
.unwrap();
rusoto_ses::Body {
text: Some(rusoto_ses::Content {
data: format!("Hi,
Someone (hopefully you) asked to reset your password on the Pijul Nest. If this wasn't you, you can just ignore this email.
Else, click on the following link to reset your password:
https://{}/password_reset?token={}
", config.host, token),
charset: Some(charset.clone())
}),
html: Some(rusoto_ses::Content {
data: body_html,
charset: Some(charset)
})
}
};
Ok(crate::email::send_email(config, subject, body, address).await?)
}
repository_cache_size = 1024
change_cache_size = 1024
max_body_length = 1048576 # 1M
hard_max_body_length = 10485760
lock_file = "lock_file"
repositories_path = "repositories"
profile_pictures_path = "profile_pictures"
host = "localhost"
version_time = "Sun, 09 Mar 2025 12:00:00 GMT"
failed_auth_timeout_millis = 10
partial_change_size = 1048576
max_password_attempts = 20
[email]
source = "no-reply@nest.pijul.com"
[time]
max_relative_days = 2
yesterday_threshold_hours = 7
[http]
http_port = 8000
https_port = 8001
ws_port = 8080
ws_timeout_secs = 3600
time_file = "./time"
log_file = "logs"
timeout_secs = 20
[ssh]
port = 2222
timeout_secs = 120
[prometheus]
buckets_start = 0.0
buckets_width = 10
n_buckets = 50
[package]
name = "nest"
version = "0.1.0"
edition = "2021"
[dependencies]
ammonia = "4.0.0"
anyhow = "1.0.95"
axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10.0", features = [ "cookie-signed", "cookie", "typed-header" ] }
axum-helmet = "0.1.0"
axum-macros = "0.5.0"
axum-response-cache = "0.2.0"
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum_csrf = "0.11.0"
bincode = "1.3.3"
bitflags = "2.8.0"
bs58 = "0.5.1"
byteorder = "1.5.0"
bytes = "1.10.0"
chrono = "0.4.39"
clap = { version = "4.5.28", features = ["derive"] }
cuach = "0.9.0"
data-encoding = "2.8.0"
deadpool = "0.12.2"
diesel = { version = "2.2.7", features = [ "postgres_backend", "extras", "network-address" ] }
diesel-async = { version = "0.5.2", features = [ "postgres", "deadpool" ] }
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
futures = "0.3.31"
headers = "0.4.0"
heapsize = "0.4.2"
helmet-core = "0.1.0"
http = "1.2.0"
http-body-util = "0.1.2"
httpdate = "1.0.3"
hyper = "1.6.0"
hyper-util = "0.1.10"
ipnetwork = "0.21"
lazy_static = "1.5.0"
libpijul = "1.0.0-beta.10"
lru-cache = "0.1.2"
minify-html = "0.15.0"
openssl = "0.10.70"
percent-encoding = "2.3.1"
pijul-hooks = { version = "0.1.2", path = "../hooks" }
privdrop = "0.5.4"
pulldown-cmark = "0.12.2"
rand = "0.9.0"
rand_chacha = "0.9.0"
regex = "1.11.1"
replication = { path = "../replication" }
reqwest = "0.12.12"
rusoto_core = "0.48.0"
rusoto_credential = "0.48.0"
rusoto_ses = "0.48.0"
sanakirja = "1.4.3"
serde = "1.0.217"
serde_derive = "1.0.217"
serde_json = "1.0.138"
serde_urlencoded = "0.7.1"
sha2 = "0.10.8"
syntect = "5.2.0"
tempfile = "3.16.0"
thiserror = "2.0.11"
thrussh = { version = "0.35.6", features = ["openssl"] }
thrussh-keys = { version = "0.22", features = ["openssl"] }
tokio = { version = "1.43.0", features = ["full"] }
tokio-postgres = "0.7.13"
tokio-stream = "0.1.17"
toml = "0.8.20"
tower-http = { version = "0.6.2", features = [ "trace", "cors", "compression-br", "compression-gzip", "compression-zstd", "compression-deflate" ] }
tower-service = "0.3.3"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
url = "2.5.4"
uuid = "1.13.1"
webauthn-rs = "0.5.1"
zstd-seekable = "0.1.23"
[workspace]
members = ["api", "replication", "hooks"]
resolver = "2"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if",
"cipher 0.3.0",
"cpufeatures",
"ctr 0.8.0",
"opaque-debug",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher 0.4.4",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes 0.8.4",
"cipher 0.4.4",
"ctr 0.9.2",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom 0.2.15",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy 0.7.35",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab99eae5ee58501ab236beb6f20f6ca39be615267b014899c89b2f0bc18a459"
dependencies = [
"html5ever 0.27.0",
"maplit",
"once_cell",
"tendril",
"url",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "asn1-rs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "async-compression"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"zstd",
"zstd-safe",
]
[[package]]
name = "async-trait"
version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-lc-rs"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
dependencies = [
"aws-lc-sys",
"paste",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
"fs_extra",
"paste",
]
[[package]]
name = "axum"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"axum-core 0.5.0",
"axum-macros",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.8.4",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b"
dependencies = [
"axum 0.8.1",
"axum-core 0.5.0",
"bytes",
"cookie",
"futures-util",
"headers",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"serde",
"tower 0.5.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-helmet"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39893950267de4634ab036e4bb7efe0f4adeedb39e5f75c04131228bcc1e53a8"
dependencies = [
"axum 0.7.9",
"helmet-core",
"http 1.2.0",
"pin-project-lite",
"tokio",
"tower 0.4.13",
"tower-service",
]
[[package]]
name = "axum-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "axum-response-cache"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f33dea5d635aa4313a4bd827f9bfd26475bbfacb2213a236ce0fbc7c4738193"
dependencies = [
"axum 0.8.1",
"cached",
"http 1.2.0",
"tower 0.5.2",
"tracing",
"tracing-futures",
]
[[package]]
name = "axum-server"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8"
dependencies = [
"arc-swap",
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower 0.4.13",
"tower-service",
]
[[package]]
name = "axum_csrf"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159933b8c6d6c161cc8d6e1dc6ba7977e8b741dbf27f7294054d3ab1ca944f54"
dependencies = [
"async-trait",
"axum-core 0.5.0",
"base64ct",
"cookie",
"hmac 0.12.1",
"http 1.2.0",
"rand 0.8.5",
"sha2 0.10.8",
"thiserror 2.0.11",
"time",
]
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64-simd"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
dependencies = [
"simd-abstraction",
]
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "base64urlsafedata"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f0ad38ce7fbed55985ad5b2197f05cff8324ee6eb6638304e78f0108fae56c"
dependencies = [
"base64 0.21.7",
"paste",
"serde",
]
[[package]]
name = "bcrypt-pbkdf"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
dependencies = [
"blowfish",
"pbkdf2 0.12.2",
"sha2 0.10.8",
]
[[package]]
name = "bigdecimal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c"
dependencies = [
"autocfg",
"libm",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.98",
"which",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "blake3"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-modes"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e"
dependencies = [
"block-padding",
"cipher 0.3.0",
]
[[package]]
name = "block-padding"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher 0.4.4",
]
[[package]]
name = "brotli"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bs58"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "bstr"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "cached"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae"
dependencies = [
"ahash 0.8.11",
"cached_proc_macro",
"cached_proc_macro_types",
"hashbrown 0.14.5",
"once_cell",
"thiserror 1.0.69",
"web-time",
]
[[package]]
name = "cached_proc_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "cached_proc_macro_types"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "canonical-path"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e9e01327e6c86e92ec72b1c798d4a94810f147209bbe3ffab6a86954937a6f"
[[package]]
name = "cc"
version = "1.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chardetng"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cmake"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "compact_jwt"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"hex",
"openssl",
"serde",
"serde_json",
"tracing",
"url",
"uuid",
]
[[package]]
name = "const-str"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
dependencies = [
"const-str-proc-macro",
]
[[package]]
name = "const-str-proc-macro"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"aes-gcm",
"base64 0.22.1",
"hmac 0.12.1",
"percent-encoding",
"rand 0.8.5",
"sha2 0.10.8",
"subtle",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "cryptovec"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc7fa13a6bbb2322d325292c57f4c8e7291595506f8289968a0eb61c3130bdf"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "cssparser"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.3",
"smallvec",
]
[[package]]
name = "cssparser-color"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
dependencies = [
"cssparser",
]
[[package]]
name = "cssparser-macros"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.98",
]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher 0.3.0",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "cuach"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d5b9d6938eb5a15d54970ffcc7d1c4572c9ba1038cf418c532172598b767524"
dependencies = [
"cuach-derive",
"uuid",
"v_htmlescape",
]
[[package]]
name = "cuach-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde8447bfd25c515bb602d8a39dbee38c6b2b2b813bc050f2fb1f88069290f91"
dependencies = [
"html5ever 0.26.0",
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
]
[[package]]
name = "curve25519-dalek"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [
"byteorder",
"digest 0.9.0",
"rand_core 0.5.1",
"serde",
"subtle",
"zeroize",
]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.98",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.98",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core 0.9.10",
]
[[package]]
name = "data-encoding"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "data-url"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
dependencies = [
"matches",
]
[[package]]
name = "deadpool"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f"
dependencies = [
"deadpool-runtime",
"num_cpus",
"tokio",
]
[[package]]
name = "deadpool-postgres"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9"
dependencies = [
"async-trait",
"deadpool",
"getrandom 0.2.15",
"tokio",
"tokio-postgres",
"tracing",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
dependencies = [
"tokio",
]
[[package]]
name = "der-parser"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "diesel"
version = "2.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5"
dependencies = [
"bigdecimal",
"bitflags 2.8.0",
"byteorder",
"chrono",
"diesel_derives",
"ipnetwork",
"itoa",
"libc",
"num-bigint",
"num-integer",
"num-traits",
"r2d2",
"serde_json",
"time",
"uuid",
]
[[package]]
name = "diesel-async"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb"
dependencies = [
"async-trait",
"deadpool",
"diesel",
"futures-util",
"scoped-futures",
"tokio",
"tokio-postgres",
]
[[package]]
name = "diesel-derive-enum"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "diesel_derives"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4"
dependencies = [
"diesel_table_macro_syntax",
"dsl_auto_type",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
dependencies = [
"syn 2.0.98",
]
[[package]]
name = "diffs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff116c9781d74b71b9b8958281309dd2faaeabad2f0a3df27e50bd79ce5dc805"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common",
"subtle",
]
[[package]]
name = "dirs"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "dsl_auto_type"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b"
dependencies = [
"darling",
"either",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "dtoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "dtoa-short"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
dependencies = [
"dtoa",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "ed25519"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
dependencies = [
"serde",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand 0.7.3",
"serde",
"serde_bytes",
"sha2 0.9.9",
"zeroize",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "flate2"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "globset"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
dependencies = [
"aho-corasick 1.1.3",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http 1.2.0",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "half"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash 0.8.11",
"bumpalo",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.11",
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64 0.21.7",
"bytes",
"headers-core",
"http 1.2.0",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http 1.2.0",
]
[[package]]
name = "heapsize"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1679e6ea370dee694f91f1dc469bf94cf8f52051d147aec3e1f9497c6fc22461"
dependencies = [
"winapi",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helmet-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c46a1217e7297a8e904aa68b4de65c59060583dffb8585886cdaaec214c787a"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac",
"digest 0.9.0",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "home"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
dependencies = [
"log",
"mac",
"markup5ever 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.2.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http-body 1.0.1",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
"hyper 1.6.0",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.6.0",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
"serde",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "ipnetwork"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "libm"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libpijul"
version = "1.0.0-beta.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230807b6b791be169a67d951be0a4c3f1a231281cfc2a8caddec6804e188517a"
dependencies = [
"adler32",
"aes 0.7.5",
"bincode",
"bitflags 1.3.2",
"blake3",
"bs58 0.4.0",
"byteorder",
"canonical-path",
"cfg-if",
"chardetng",
"chrono",
"crossbeam-deque",
"curve25519-dalek",
"data-encoding",
"diffs",
"ed25519-dalek",
"encoding_rs",
"flate2",
"generic-array",
"getrandom 0.2.15",
"hmac 0.11.0",
"ignore",
"lazy_static",
"log",
"lru-cache",
"memchr",
"nom",
"parking_lot 0.11.2",
"path-slash",
"pbkdf2 0.9.0",
"pijul-macros",
"rand 0.8.5",
"regex",
"sanakirja",
"serde",
"serde_derive",
"serde_json",
"sha2 0.9.9",
"tar",
"tempfile",
"thiserror 1.0.69",
"toml 0.5.11",
"twox-hash",
"zstd-seekable",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"libc",
"redox_syscall 0.5.8",
]
[[package]]
name = "libsodium-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
dependencies = [
"cc",
"libc",
"pkg-config",
"walkdir",
]
[[package]]
name = "lightningcss"
version = "1.0.0-alpha.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a75fcbcdbcc84fc1ae7c60c31f99337560b620757a9bfc1c9f84df3cff8ac24"
dependencies = [
"ahash 0.8.11",
"bitflags 2.8.0",
"const-str",
"cssparser",
"cssparser-color",
"dashmap",
"data-encoding",
"getrandom 0.2.15",
"indexmap",
"itertools 0.10.5",
"lazy_static",
"lightningcss-derive",
"parcel_selectors",
"parcel_sourcemap",
"paste",
"pathdiff",
"rayon",
"serde",
"smallvec",
]
[[package]]
name = "lightningcss-derive"
version = "1.0.0-alpha.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen 0.10.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [
"log",
"phf 0.11.3",
"phf_codegen 0.11.3",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
dependencies = [
"block-buffer 0.9.0",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "memmap2"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minify-html"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd4517942a8e7425c990b14977f86a63e4996eed7b15cfcca1540126ac5ff25"
dependencies = [
"aho-corasick 0.7.20",
"lazy_static",
"lightningcss",
"memchr",
"minify-html-common",
"minify-js",
"once_cell",
"rustc-hash 1.1.0",
]
[[package]]
name = "minify-html-common"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697a6b40dffdc5de10c0cbd709dc2bc2039cea9dab8aaa636eb9a49d6b411780"
dependencies = [
"aho-corasick 0.7.20",
"itertools 0.12.1",
"lazy_static",
"memchr",
"rustc-hash 1.1.0",
"serde",
"serde_json",
]
[[package]]
name = "minify-js"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22d6c512a82abddbbc13b70609cb2beff01be2c7afff534d6e5e1c85e438fc8b"
dependencies = [
"lazy_static",
"parse-js",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
name = "native-tls"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nest"
version = "0.1.0"
dependencies = [
"ammonia",
"anyhow",
"axum 0.8.1",
"axum-extra",
"axum-helmet",
"axum-macros",
"axum-response-cache",
"axum-server",
"axum_csrf",
"bincode",
"bitflags 2.8.0",
"bs58 0.5.1",
"byteorder",
"bytes",
"chrono",
"clap",
"cuach",
"data-encoding",
"deadpool",
"diesel",
"diesel-async",
"diesel-derive-enum",
"futures",
"headers",
"heapsize",
"helmet-core",
"http 1.2.0",
"http-body-util",
"httpdate",
"hyper 1.6.0",
"hyper-util",
"ipnetwork",
"lazy_static",
"libpijul",
"lru-cache",
"minify-html",
"openssl",
"percent-encoding",
"pijul-hooks",
"privdrop",
"pulldown-cmark",
"rand 0.9.0",
"rand_chacha 0.9.0",
"regex",
"replication",
"reqwest",
"rusoto_core",
"rusoto_credential",
"rusoto_ses",
"sanakirja",
"serde",
"serde_derive",
"serde_json",
"serde_urlencoded",
"sha2 0.10.8",
"syntect",
"tempfile",
"thiserror 2.0.11",
"thrussh",
"thrussh-keys",
"tokio",
"tokio-postgres",
"tokio-stream",
"toml 0.8.20",
"tower-http",
"tower-service",
"tracing",
"tracing-subscriber",
"url",
"uuid",
"webauthn-rs",
"zstd-seekable",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "outref"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parcel_selectors"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dccbc6fb560df303a44e511618256029410efbc87779018f751ef12c488271fe"
dependencies = [
"bitflags 2.8.0",
"cssparser",
"log",
"phf 0.11.3",
"phf_codegen 0.11.3",
"precomputed-hash",
"rustc-hash 2.1.1",
"smallvec",
]
[[package]]
name = "parcel_sourcemap"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
dependencies = [
"base64-simd",
"data-url",
"rkyv",
"serde",
"serde_json",
"vlq",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core 0.9.10",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.8",
"smallvec",
"windows-targets",
]
[[package]]
name = "parse-js"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ec3b11d443640ec35165ee8f6f0559f1c6f41878d70330fe9187012b5935f02"
dependencies = [
"aho-corasick 0.7.20",
"bumpalo",
"hashbrown 0.13.2",
"lazy_static",
"memchr",
]
[[package]]
name = "password-hash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "path-slash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa"
dependencies = [
"base64ct",
"crypto-mac",
"hmac 0.11.0",
"password-hash",
"sha2 0.9.9",
]
[[package]]
name = "pbkdf2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739"
dependencies = [
"crypto-mac",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_shared 0.10.0",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared 0.11.3",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher 0.3.11",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher 1.0.1",
]
[[package]]
name = "pijul-hooks"
version = "0.1.2"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "pijul-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2b3743cd80b65d40b9f20b6575016f76d4221754866316ffbf7d7b0d58e7064"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
]
[[package]]
name = "pin-project"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "plist"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "postgres-derive"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69700ea4603c5ef32d447708e6a19cd3e8ac197a000842e97f527daea5e4175f"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "postgres-openssl"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb14e4bbc2c0b3d165bf30b79c7a9c10412dff9d98491ffdd64ed810ab891d21"
dependencies = [
"openssl",
"tokio",
"tokio-openssl",
"tokio-postgres",
]
[[package]]
name = "postgres-protocol"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54"
dependencies = [
"base64 0.22.1",
"byteorder",
"bytes",
"fallible-iterator",
"hmac 0.12.1",
"md-5 0.10.6",
"memchr",
"rand 0.9.0",
"sha2 0.10.8",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48"
dependencies = [
"bytes",
"chrono",
"fallible-iterator",
"postgres-derive",
"postgres-protocol",
"uuid",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy 0.7.35",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn 2.0.98",
]
[[package]]
name = "privdrop"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bc12de3935536ed9b69488faea4450a298dac44179b54f71806e63f55034bf9"
dependencies = [
"libc",
"nix",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pulldown-cmark"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
dependencies = [
"bitflags 2.8.0",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quick-xml"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot 0.12.3",
"scheduled-thread-pool",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.17",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.17",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 1.0.69",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick 1.1.3",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick 1.1.3",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "replication"
version = "0.1.0"
dependencies = [
"bincode",
"byteorder",
"bytes",
"clap",
"deadpool-postgres",
"env_logger",
"futures",
"indexmap",
"lazy_static",
"libc",
"libpijul",
"log",
"memmap",
"openssl",
"postgres-openssl",
"postgres-types",
"rand 0.9.0",
"regex",
"serde",
"serde_derive",
"thiserror 2.0.11",
"tokio",
"tokio-openssl",
"tokio-postgres",
"toml 0.8.20",
"uuid",
"webpki",
]
[[package]]
name = "reqwest"
version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-rustls",
"hyper-tls 0.6.0",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower 0.5.2",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta",
"rend",
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rusoto_core"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2"
dependencies = [
"async-trait",
"base64 0.13.1",
"bytes",
"crc32fast",
"futures",
"http 0.2.12",
"hyper 0.14.32",
"hyper-tls 0.5.0",
"lazy_static",
"log",
"rusoto_credential",
"rusoto_signature",
"rustc_version",
"serde",
"serde_json",
"tokio",
"xml-rs",
]
[[package]]
name = "rusoto_credential"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05"
dependencies = [
"async-trait",
"chrono",
"dirs-next",
"futures",
"hyper 0.14.32",
"serde",
"serde_json",
"shlex",
"tokio",
"zeroize",
]
[[package]]
name = "rusoto_ses"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c365601ac8eab93ff446198a1237c6c2e3d572226cad7fe094d5dcd87d7406cd"
dependencies = [
"async-trait",
"bytes",
"futures",
"rusoto_core",
"serde_urlencoded",
"xml-rs",
]
[[package]]
name = "rusoto_signature"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272"
dependencies = [
"base64 0.13.1",
"bytes",
"chrono",
"digest 0.9.0",
"futures",
"hex",
"hmac 0.11.0",
"http 0.2.12",
"hyper 0.14.32",
"log",
"md-5 0.9.1",
"percent-encoding",
"pin-project-lite",
"rusoto_credential",
"rustc_version",
"serde",
"sha2 0.9.9",
"tokio",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustls"
version = "0.23.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
dependencies = [
"aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "sanakirja"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81aaf70d064e2122209f04d01fd91e8908e7a327b516236e1cbc0c3f34ac6d11"
dependencies = [
"crc32fast",
"fs2",
"lazy_static",
"log",
"memmap2",
"parking_lot 0.11.2",
"sanakirja-core",
"serde",
"thiserror 1.0.69",
]
[[package]]
name = "sanakirja-core"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8376db34ae3eac6e7bd91168bc638450073b708ce9fb46940de676f552238bf5"
dependencies = [
"crc32fast",
]
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
dependencies = [
"parking_lot 0.12.3",
]
[[package]]
name = "scoped-futures"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b24aae2d0636530f359e9d5ef0c04669d11c5e756699b27a6a6d845d8329091"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.8.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
dependencies = [
"serde",
]
[[package]]
name = "serde_cbor_2"
version = "0.12.0-dev"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "serde_json"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
[[package]]
name = "simd-abstraction"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
dependencies = [
"outref",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_cache"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe"
dependencies = [
"new_debug_unreachable",
"parking_lot 0.12.3",
"phf_shared 0.11.3",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "244292f3441c89febe5b5bdfbb6863aeaf4f64da810ea3050fd927b27b8d92ce"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
]
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "syntect"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
dependencies = [
"bincode",
"bitflags 1.3.2",
"flate2",
"fnv",
"once_cell",
"onig",
"plist",
"regex-syntax 0.8.5",
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
"walkdir",
"yaml-rust",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.8.0",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.11",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "thrussh"
version = "0.35.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2127fc8654db70967c556bc9bf5dfef85997b1b901f3d82e9880c39d1051b278"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"cryptovec",
"digest 0.9.0",
"flate2",
"futures",
"generic-array",
"log",
"openssl",
"rand 0.8.5",
"sha2 0.9.9",
"thiserror 1.0.69",
"thrussh-keys",
"thrussh-libsodium",
"tokio",
]
[[package]]
name = "thrussh-keys"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43d59b13e4c08db0e379bced99bda596ac5ed33651d919bf3916d34ad4259bb"
dependencies = [
"aes 0.7.5",
"bcrypt-pbkdf",
"bit-vec",
"block-modes",
"byteorder",
"cryptovec",
"data-encoding",
"dirs",
"futures",
"hmac 0.11.0",
"log",
"md5",
"num-bigint",
"num-integer",
"openssl",
"pbkdf2 0.8.0",
"rand 0.8.5",
"serde",
"serde_derive",
"sha2 0.9.9",
"thiserror 1.0.69",
"thrussh-libsodium",
"tokio",
"tokio-stream",
"yasna",
]
[[package]]
name = "thrussh-libsodium"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f9c0eb4cff7225e782f7c4930c7b1f9caedf45c182e2d8602c0ec34679a1e"
dependencies = [
"lazy_static",
"libc",
"libsodium-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot 0.12.3",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-openssl"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd"
dependencies = [
"openssl",
"openssl-sys",
"tokio",
]
[[package]]
name = "tokio-postgres"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0"
dependencies = [
"async-trait",
"byteorder",
"bytes",
"fallible-iterator",
"futures-channel",
"futures-util",
"log",
"parking_lot 0.12.3",
"percent-encoding",
"phf 0.11.3",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand 0.9.0",
"socket2",
"tokio",
"tokio-util",
"whoami",
]
[[package]]
name = "tokio-rustls"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"async-compression",
"bitflags 2.8.0",
"bytes",
"futures-core",
"http 1.2.0",
"http-body 1.0.1",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"rand 0.8.5",
"static_assertions",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-normalization"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [
"getrandom 0.3.1",
"serde",
]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vlq"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.98",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webauthn-attestation-ca"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29e77e8859ecb93b00e4a8e56ae45f8a8dd69b1539e3d32cf4cce1db9a3a0b99"
dependencies = [
"base64urlsafedata",
"openssl",
"serde",
"tracing",
"uuid",
]
[[package]]
name = "webauthn-rs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b44347ee0d66f222043663a6aaf5ec78022b9b11c3a9ed488c21f2bd5680856"
dependencies = [
"base64urlsafedata",
"serde",
"tracing",
"url",
"uuid",
"webauthn-rs-core",
]
[[package]]
name = "webauthn-rs-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ef48f07ed8f3dfe304d6c48e85317feba0439675f31a13063b2936c9b4eaf0d"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"compact_jwt",
"der-parser",
"hex",
"nom",
"openssl",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"serde_cbor_2",
"serde_json",
"thiserror 1.0.69",
"tracing",
"url",
"uuid",
"webauthn-attestation-ca",
"webauthn-rs-proto",
"x509-parser",
]
[[package]]
name = "webauthn-rs-proto"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14e1367f70e7dc7b83afc971ce8a54d578f4fdf488ea093021180e073744a69f"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"serde",
"serde_json",
"url",
]
[[package]]
name = "webpki"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
]
[[package]]
name = "whoami"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
dependencies = [
"redox_syscall 0.5.8",
"wasite",
"web-sys",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x509-parser"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
dependencies = [
"asn1-rs",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "xattr"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
dependencies = [
"libc",
"linux-raw-sys",
"rustix",
]
[[package]]
name = "xml-rs"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yasna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75"
dependencies = [
"bit-vec",
"num-bigint",
]
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713"
dependencies = [
"zerocopy-derive 0.8.17",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-seekable"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "574a117c5cdb88d1f13381ee3a19a6a45fb6ca0c98436d3a95df852b7ca6c3c2"
dependencies = [
"bincode",
"cc",
"libc",
"pkg-config",
"serde",
"serde_derive",
"thiserror 1.0.69",
"threadpool",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",
]
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.