diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 157e409c8..7fd2c358e 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,45 +1,51 @@
-
+
-### Changes
-
-
+## Changes
+
+
-
-### Issues
-
+## Issues
-- fixes:
+
-### Category
-
-- [x] Bug fix
-- [x] New feature
-- [x] Adding new one click service
-- [x] Fixing or updating existing one click service
+- Fixes
-### Screenshots or Video (if applicable)
-
-
+## Category
-### AI Usage
-
-
+- [ ] Bug fix
+- [ ] Improvement
+- [ ] New feature
+- [ ] Adding new one click service
+- [ ] Fixing or updating existing one click service
-- [x] AI is used in the process of creating this PR
-- [x] AI is NOT used in the process of creating this PR
+## Preview
-### Steps to Test
-
-
+
-- Step 1 – what to do first
-- Step 2 – next action
+## AI Assistance
-### Contributor Agreement
-
+
+
+- [ ] AI was NOT used to create this PR
+- [ ] AI was used (please describe below)
+
+**If AI was used:**
+
+- Tools used:
+- How extensively:
+
+## Testing
+
+
+
+## Contributor Agreement
+
+
> [!IMPORTANT]
>
-> - [x] I have read and understood the [contributor guidelines](https://github.com/coollabsio/coolify/blob/v4.x/CONTRIBUTING.md). If I have failed to follow any guideline, I understand that this PR may be closed without review.
-> - [x] I have tested the changes thoroughly and am confident that they will work as expected without issues when the maintainer tests them
+> - [ ] I have read and understood the [contributor guidelines](https://github.com/coollabsio/coolify/blob/v4.x/CONTRIBUTING.md). If I have failed to follow any guideline, I understand that this PR may be closed without review.
+> - [ ] I have searched [existing issues](https://github.com/coollabsio/coolify/issues) and [pull requests](https://github.com/coollabsio/coolify/pulls) (including closed ones) to ensure this isn't a duplicate.
+> - [ ] I have tested all the changes thoroughly with a local development instance of Coolify and I am confident that they will work as expected when a maintainer tests them.
diff --git a/.github/workflows/pr-quality.yaml b/.github/workflows/pr-quality.yaml
index d264ad470..594724fdb 100644
--- a/.github/workflows/pr-quality.yaml
+++ b/.github/workflows/pr-quality.yaml
@@ -16,7 +16,7 @@ jobs:
- uses: peakoss/anti-slop@v0
with:
# General Settings
- max-failures: 3
+ max-failures: 4
# PR Branch Checks
allowed-target-branches: "next"
@@ -26,7 +26,6 @@ jobs:
main
master
v4.x
- next
# PR Quality Checks
max-negative-reactions: 0
@@ -37,16 +36,24 @@ jobs:
# PR Description Checks
require-description: true
- max-description-length: 0
+ max-description-length: 2500
max-emoji-count: 2
- require-pr-template: true
+ max-code-references: 5
require-linked-issue: false
blocked-terms: "STRAWBERRY"
blocked-issue-numbers: 8154
+ # PR Template Checks
+ require-pr-template: true
+ strict-pr-template-sections: "Contributor Agreement"
+ optional-pr-template-sections: "Issues,Preview"
+ max-additional-pr-template-sections: 2
+
# Commit Message Checks
+ max-commit-message-length: 500
require-conventional-commits: false
- blocked-commit-authors: "claude,copilot"
+ require-commit-author-match: true
+ blocked-commit-authors: ""
# File Checks
allowed-file-extensions: ""
@@ -59,38 +66,43 @@ jobs:
templates/service-templates-latest.json
templates/service-templates.json
require-final-newline: true
+ max-added-comments: 10
- # User Health Checks
+ # User Checks
+ detect-spam-usernames: true
+ min-account-age: 30
+ max-daily-forks: 7
+ min-profile-completeness: 4
+
+ # Merge Checks
min-repo-merged-prs: 0
min-repo-merge-ratio: 0
min-global-merge-ratio: 30
global-merge-ratio-exclude-own: false
- min-account-age: 10
# Exemptions
- exempt-author-association: "OWNER,MEMBER,COLLABORATOR"
- exempt-users: ""
+ exempt-draft-prs: false
exempt-bots: |
actions-user
dependabot[bot]
renovate[bot]
github-actions[bot]
- exempt-draft-prs: false
+ exempt-users: ""
+ exempt-author-association: "OWNER,MEMBER,COLLABORATOR"
exempt-label: "quality/exempt"
exempt-pr-label: ""
- exempt-milestones: ""
- exempt-pr-milestones: ""
exempt-all-milestones: false
exempt-all-pr-milestones: false
+ exempt-milestones: ""
+ exempt-pr-milestones: ""
# PR Success Actions
success-add-pr-labels: "quality/verified"
# PR Failure Actions
- close-pr: true
- lock-pr: false
- delete-branch: false
- failure-pr-message: "This PR did not pass quality checks so it will be closed. If you believe this is a mistake please let us know."
failure-remove-pr-labels: ""
failure-remove-all-pr-labels: true
failure-add-pr-labels: "quality/rejected"
+ failure-pr-message: "This PR did not pass quality checks so it will be closed. If you believe this is a mistake please let us know."
+ close-pr: true
+ lock-pr: false
diff --git a/README.md b/README.md
index c78e47997..e9ea0e7d4 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,9 @@ ## Donations
### Huge Sponsors
+* [MVPS](https://www.mvps.net?ref=coolify.io) - Cheap VPS servers at the highest possible quality
* [SerpAPI](https://serpapi.com?ref=coolify.io) - Google Search API — Scrape Google and other search engines from our fast, easy, and complete API
+*
### Big Sponsors
@@ -85,7 +87,6 @@ ### Big Sponsors
* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions
* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
-* [MVPS](https://www.mvps.net?ref=coolify.io) - Cheap VPS servers at the highest possible quality
* [Mobb](https://vibe.mobb.ai/?ref=coolify.io) - Secure Your AI-Generated Code to Unlock Dev Productivity
* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
* [Ramnode](https://ramnode.com/?ref=coolify.io) - High Performance Cloud VPS Hosting
@@ -96,6 +97,7 @@ ### Big Sponsors
* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform
* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
+* [VPSDime](https://vpsdime.com?ref=coolify.io) - Affordable high-performance VPS hosting solutions
### Small Sponsors
diff --git a/app/Actions/Stripe/CancelSubscriptionAtPeriodEnd.php b/app/Actions/Stripe/CancelSubscriptionAtPeriodEnd.php
new file mode 100644
index 000000000..34c7d194a
--- /dev/null
+++ b/app/Actions/Stripe/CancelSubscriptionAtPeriodEnd.php
@@ -0,0 +1,60 @@
+stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
+ }
+
+ /**
+ * Cancel the team's subscription at the end of the current billing period.
+ *
+ * @return array{success: bool, error: string|null}
+ */
+ public function execute(Team $team): array
+ {
+ $subscription = $team->subscription;
+
+ if (! $subscription?->stripe_subscription_id) {
+ return ['success' => false, 'error' => 'No active subscription found.'];
+ }
+
+ if (! $subscription->stripe_invoice_paid) {
+ return ['success' => false, 'error' => 'Subscription is not active.'];
+ }
+
+ if ($subscription->stripe_cancel_at_period_end) {
+ return ['success' => false, 'error' => 'Subscription is already set to cancel at the end of the billing period.'];
+ }
+
+ try {
+ $this->stripe->subscriptions->update($subscription->stripe_subscription_id, [
+ 'cancel_at_period_end' => true,
+ ]);
+
+ $subscription->update([
+ 'stripe_cancel_at_period_end' => true,
+ ]);
+
+ \Log::info("Subscription {$subscription->stripe_subscription_id} set to cancel at period end for team {$team->name}");
+
+ return ['success' => true, 'error' => null];
+ } catch (\Stripe\Exception\InvalidRequestException $e) {
+ \Log::error("Stripe cancel at period end error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+ } catch (\Exception $e) {
+ \Log::error("Cancel at period end error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'An unexpected error occurred. Please contact support.'];
+ }
+ }
+}
diff --git a/app/Actions/Stripe/RefundSubscription.php b/app/Actions/Stripe/RefundSubscription.php
new file mode 100644
index 000000000..021cba13e
--- /dev/null
+++ b/app/Actions/Stripe/RefundSubscription.php
@@ -0,0 +1,141 @@
+stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
+ }
+
+ /**
+ * Check if the team's subscription is eligible for a refund.
+ *
+ * @return array{eligible: bool, days_remaining: int, reason: string}
+ */
+ public function checkEligibility(Team $team): array
+ {
+ $subscription = $team->subscription;
+
+ if ($subscription?->stripe_refunded_at) {
+ return $this->ineligible('A refund has already been processed for this team.');
+ }
+
+ if (! $subscription?->stripe_subscription_id) {
+ return $this->ineligible('No active subscription found.');
+ }
+
+ if (! $subscription->stripe_invoice_paid) {
+ return $this->ineligible('Subscription invoice is not paid.');
+ }
+
+ try {
+ $stripeSubscription = $this->stripe->subscriptions->retrieve($subscription->stripe_subscription_id);
+ } catch (\Stripe\Exception\InvalidRequestException $e) {
+ return $this->ineligible('Subscription not found in Stripe.');
+ }
+
+ if (! in_array($stripeSubscription->status, ['active', 'trialing'])) {
+ return $this->ineligible("Subscription status is '{$stripeSubscription->status}'.");
+ }
+
+ $startDate = \Carbon\Carbon::createFromTimestamp($stripeSubscription->start_date);
+ $daysSinceStart = (int) $startDate->diffInDays(now());
+ $daysRemaining = self::REFUND_WINDOW_DAYS - $daysSinceStart;
+
+ if ($daysRemaining <= 0) {
+ return $this->ineligible('The 30-day refund window has expired.');
+ }
+
+ return [
+ 'eligible' => true,
+ 'days_remaining' => $daysRemaining,
+ 'reason' => 'Eligible for refund.',
+ ];
+ }
+
+ /**
+ * Process a full refund and cancel the subscription.
+ *
+ * @return array{success: bool, error: string|null}
+ */
+ public function execute(Team $team): array
+ {
+ $eligibility = $this->checkEligibility($team);
+
+ if (! $eligibility['eligible']) {
+ return ['success' => false, 'error' => $eligibility['reason']];
+ }
+
+ $subscription = $team->subscription;
+
+ try {
+ $invoices = $this->stripe->invoices->all([
+ 'subscription' => $subscription->stripe_subscription_id,
+ 'status' => 'paid',
+ 'limit' => 1,
+ ]);
+
+ if (empty($invoices->data)) {
+ return ['success' => false, 'error' => 'No paid invoice found to refund.'];
+ }
+
+ $invoice = $invoices->data[0];
+ $paymentIntentId = $invoice->payment_intent;
+
+ if (! $paymentIntentId) {
+ return ['success' => false, 'error' => 'No payment intent found on the invoice.'];
+ }
+
+ $this->stripe->refunds->create([
+ 'payment_intent' => $paymentIntentId,
+ ]);
+
+ $this->stripe->subscriptions->cancel($subscription->stripe_subscription_id);
+
+ $subscription->update([
+ 'stripe_cancel_at_period_end' => false,
+ 'stripe_invoice_paid' => false,
+ 'stripe_trial_already_ended' => false,
+ 'stripe_past_due' => false,
+ 'stripe_feedback' => 'Refund requested by user',
+ 'stripe_comment' => 'Full refund processed within 30-day window at '.now()->toDateTimeString(),
+ 'stripe_refunded_at' => now(),
+ ]);
+
+ $team->subscriptionEnded();
+
+ \Log::info("Refunded and cancelled subscription {$subscription->stripe_subscription_id} for team {$team->name}");
+
+ return ['success' => true, 'error' => null];
+ } catch (\Stripe\Exception\InvalidRequestException $e) {
+ \Log::error("Stripe refund error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+ } catch (\Exception $e) {
+ \Log::error("Refund error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'An unexpected error occurred. Please contact support.'];
+ }
+ }
+
+ /**
+ * @return array{eligible: bool, days_remaining: int, reason: string}
+ */
+ private function ineligible(string $reason): array
+ {
+ return [
+ 'eligible' => false,
+ 'days_remaining' => 0,
+ 'reason' => $reason,
+ ];
+ }
+}
diff --git a/app/Actions/Stripe/ResumeSubscription.php b/app/Actions/Stripe/ResumeSubscription.php
new file mode 100644
index 000000000..d8019def7
--- /dev/null
+++ b/app/Actions/Stripe/ResumeSubscription.php
@@ -0,0 +1,56 @@
+stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
+ }
+
+ /**
+ * Resume a subscription that was set to cancel at the end of the billing period.
+ *
+ * @return array{success: bool, error: string|null}
+ */
+ public function execute(Team $team): array
+ {
+ $subscription = $team->subscription;
+
+ if (! $subscription?->stripe_subscription_id) {
+ return ['success' => false, 'error' => 'No active subscription found.'];
+ }
+
+ if (! $subscription->stripe_cancel_at_period_end) {
+ return ['success' => false, 'error' => 'Subscription is not set to cancel.'];
+ }
+
+ try {
+ $this->stripe->subscriptions->update($subscription->stripe_subscription_id, [
+ 'cancel_at_period_end' => false,
+ ]);
+
+ $subscription->update([
+ 'stripe_cancel_at_period_end' => false,
+ ]);
+
+ \Log::info("Subscription {$subscription->stripe_subscription_id} resumed for team {$team->name}");
+
+ return ['success' => true, 'error' => null];
+ } catch (\Stripe\Exception\InvalidRequestException $e) {
+ \Log::error("Stripe resume subscription error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+ } catch (\Exception $e) {
+ \Log::error("Resume subscription error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'An unexpected error occurred. Please contact support.'];
+ }
+ }
+}
diff --git a/app/Actions/Stripe/UpdateSubscriptionQuantity.php b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
new file mode 100644
index 000000000..c181e988d
--- /dev/null
+++ b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
@@ -0,0 +1,197 @@
+stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
+ }
+
+ /**
+ * Fetch a full price preview for a quantity change from Stripe.
+ * Returns both the prorated amount due now and the recurring cost for the next billing cycle.
+ *
+ * @return array{success: bool, error: string|null, preview: array{due_now: int, recurring_subtotal: int, recurring_tax: int, recurring_total: int, unit_price: int, tax_description: string|null, quantity: int, currency: string}|null}
+ */
+ public function fetchPricePreview(Team $team, int $quantity): array
+ {
+ $subscription = $team->subscription;
+
+ if (! $subscription?->stripe_subscription_id || ! $subscription->stripe_invoice_paid) {
+ return ['success' => false, 'error' => 'No active subscription found.', 'preview' => null];
+ }
+
+ try {
+ $stripeSubscription = $this->stripe->subscriptions->retrieve($subscription->stripe_subscription_id);
+ $item = $stripeSubscription->items->data[0] ?? null;
+
+ if (! $item) {
+ return ['success' => false, 'error' => 'Could not retrieve subscription details.', 'preview' => null];
+ }
+
+ $currency = strtoupper($item->price->currency ?? 'usd');
+
+ // Upcoming invoice gives us the prorated amount due now
+ $upcomingInvoice = $this->stripe->invoices->upcoming([
+ 'customer' => $subscription->stripe_customer_id,
+ 'subscription' => $subscription->stripe_subscription_id,
+ 'subscription_items' => [
+ ['id' => $item->id, 'quantity' => $quantity],
+ ],
+ 'subscription_proration_behavior' => 'create_prorations',
+ ]);
+
+ // Extract tax percentage — try total_tax_amounts first, fall back to invoice tax/subtotal
+ $taxPercentage = 0.0;
+ $taxDescription = null;
+ if (! empty($upcomingInvoice->total_tax_amounts)) {
+ $taxAmount = $upcomingInvoice->total_tax_amounts[0] ?? null;
+ if ($taxAmount?->tax_rate) {
+ $taxRate = $this->stripe->taxRates->retrieve($taxAmount->tax_rate);
+ $taxPercentage = (float) ($taxRate->percentage ?? 0);
+ $taxDescription = $taxRate->display_name.' ('.$taxRate->jurisdiction.') '.$taxRate->percentage.'%';
+ }
+ }
+ // Fallback tax percentage from invoice totals - use tax_rate details when available for accuracy
+ if ($taxPercentage === 0.0 && ($upcomingInvoice->tax ?? 0) > 0 && ($upcomingInvoice->subtotal ?? 0) > 0) {
+ $taxPercentage = round(($upcomingInvoice->tax / $upcomingInvoice->subtotal) * 100, 2);
+ }
+
+ // Recurring cost for next cycle — read from non-proration invoice lines
+ $recurringSubtotal = 0;
+ foreach ($upcomingInvoice->lines->data as $line) {
+ if (! $line->proration) {
+ $recurringSubtotal += $line->amount;
+ }
+ }
+ $unitPrice = $quantity > 0 ? (int) round($recurringSubtotal / $quantity) : 0;
+
+ $recurringTax = $taxPercentage > 0
+ ? (int) round($recurringSubtotal * $taxPercentage / 100)
+ : 0;
+ $recurringTotal = $recurringSubtotal + $recurringTax;
+
+ // Due now = amount_due (accounts for customer balance/credits) minus recurring
+ $amountDue = $upcomingInvoice->amount_due ?? $upcomingInvoice->total ?? 0;
+ $dueNow = $amountDue - $recurringTotal;
+
+ return [
+ 'success' => true,
+ 'error' => null,
+ 'preview' => [
+ 'due_now' => $dueNow,
+ 'recurring_subtotal' => $recurringSubtotal,
+ 'recurring_tax' => $recurringTax,
+ 'recurring_total' => $recurringTotal,
+ 'unit_price' => $unitPrice,
+ 'tax_description' => $taxDescription,
+ 'quantity' => $quantity,
+ 'currency' => $currency,
+ ],
+ ];
+ } catch (\Exception $e) {
+ \Log::warning("Stripe fetch price preview error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'Could not load price preview.', 'preview' => null];
+ }
+ }
+
+ /**
+ * Update the subscription quantity (server limit) for a team.
+ *
+ * @return array{success: bool, error: string|null}
+ */
+ public function execute(Team $team, int $quantity): array
+ {
+ if ($quantity < self::MIN_SERVER_LIMIT) {
+ return ['success' => false, 'error' => 'Minimum server limit is '.self::MIN_SERVER_LIMIT.'.'];
+ }
+
+ $subscription = $team->subscription;
+
+ if (! $subscription?->stripe_subscription_id) {
+ return ['success' => false, 'error' => 'No active subscription found.'];
+ }
+
+ if (! $subscription->stripe_invoice_paid) {
+ return ['success' => false, 'error' => 'Subscription is not active.'];
+ }
+
+ try {
+ $stripeSubscription = $this->stripe->subscriptions->retrieve($subscription->stripe_subscription_id);
+ $item = $stripeSubscription->items->data[0] ?? null;
+
+ if (! $item?->id) {
+ return ['success' => false, 'error' => 'Could not find subscription item.'];
+ }
+
+ $previousQuantity = $item->quantity ?? $team->custom_server_limit;
+
+ $updatedSubscription = $this->stripe->subscriptions->update($subscription->stripe_subscription_id, [
+ 'items' => [
+ ['id' => $item->id, 'quantity' => $quantity],
+ ],
+ 'proration_behavior' => 'always_invoice',
+ 'expand' => ['latest_invoice'],
+ ]);
+
+ // Check if the proration invoice was paid
+ $latestInvoice = $updatedSubscription->latest_invoice;
+ if ($latestInvoice && $latestInvoice->status !== 'paid') {
+ \Log::warning("Subscription {$subscription->stripe_subscription_id} quantity updated but invoice not paid (status: {$latestInvoice->status}) for team {$team->name}. Reverting to {$previousQuantity}.");
+
+ // Revert subscription quantity on Stripe
+ $this->stripe->subscriptions->update($subscription->stripe_subscription_id, [
+ 'items' => [
+ ['id' => $item->id, 'quantity' => $previousQuantity],
+ ],
+ 'proration_behavior' => 'none',
+ ]);
+
+ // Void the unpaid invoice
+ if ($latestInvoice->id) {
+ $this->stripe->invoices->voidInvoice($latestInvoice->id);
+ }
+
+ return ['success' => false, 'error' => 'Payment failed. Your server limit was not changed. Please check your payment method and try again.'];
+ }
+
+ $team->update([
+ 'custom_server_limit' => $quantity,
+ ]);
+
+ ServerLimitCheckJob::dispatch($team);
+
+ \Log::info("Subscription {$subscription->stripe_subscription_id} quantity updated to {$quantity} for team {$team->name}");
+
+ return ['success' => true, 'error' => null];
+ } catch (\Stripe\Exception\InvalidRequestException $e) {
+ \Log::error("Stripe update quantity error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+ } catch (\Exception $e) {
+ \Log::error("Update subscription quantity error for team {$team->id}: ".$e->getMessage());
+
+ return ['success' => false, 'error' => 'An unexpected error occurred. Please contact support.'];
+ }
+ }
+
+ private function formatAmount(int $cents, string $currency): string
+ {
+ return strtoupper($currency) === 'USD'
+ ? '$'.number_format($cents / 100, 2)
+ : number_format($cents / 100, 2).' '.$currency;
+ }
+}
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index 723c6d4a5..54d5714a6 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -37,7 +37,7 @@ public static function ensureMultiplexedConnection(Server $server): bool
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
- $checkCommand .= "{$server->user}@{$server->ip}";
+ $checkCommand .= self::escapedUserAtHost($server);
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
@@ -80,7 +80,7 @@ public static function establishNewMultiplexedConnection(Server $server): bool
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
- $establishCommand .= "{$server->user}@{$server->ip}";
+ $establishCommand .= self::escapedUserAtHost($server);
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
return false;
@@ -101,7 +101,7 @@ public static function removeMuxFile(Server $server)
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
- $closeCommand .= "{$server->user}@{$server->ip}";
+ $closeCommand .= self::escapedUserAtHost($server);
Process::run($closeCommand);
// Clear connection metadata from cache
@@ -141,9 +141,9 @@ public static function generateScpCommand(Server $server, string $source, string
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
if ($server->isIpv6()) {
- $scp_command .= "{$source} {$server->user}@[{$server->ip}]:{$dest}";
+ $scp_command .= "{$source} ".escapeshellarg($server->user).'@['.escapeshellarg($server->ip)."]:{$dest}";
} else {
- $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
+ $scp_command .= "{$source} ".self::escapedUserAtHost($server).":{$dest}";
}
return $scp_command;
@@ -189,13 +189,18 @@ public static function generateSshCommand(Server $server, string $command, bool
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command);
- $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
+ $ssh_command .= self::escapedUserAtHost($server)." 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
+ private static function escapedUserAtHost(Server $server): string
+ {
+ return escapeshellarg($server->user).'@'.escapeshellarg($server->ip);
+ }
+
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
@@ -224,9 +229,9 @@ private static function getCommonSshOptions(Server $server, string $sshKeyLocati
// Bruh
if ($isScp) {
- $options .= "-P {$server->port} ";
+ $options .= '-P '.escapeshellarg((string) $server->port).' ';
} else {
- $options .= "-p {$server->port} ";
+ $options .= '-p '.escapeshellarg((string) $server->port).' ';
}
return $options;
@@ -245,7 +250,7 @@ public static function isConnectionHealthy(Server $server): bool
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$healthCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
- $healthCommand .= "{$server->user}@{$server->ip} 'echo \"health_check_ok\"'";
+ $healthCommand .= self::escapedUserAtHost($server)." 'echo \"health_check_ok\"'";
$process = Process::run($healthCommand);
$isHealthy = $process->exitCode() === 0 && str_contains($process->output(), 'health_check_ok');
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index ddef74226..eb2c15625 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -1095,6 +1095,17 @@ private function create_application(Request $request, $type)
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
+ if ($destinations->count() > 1 && $request->has('destination_uuid')) {
+ $destination = $destinations->where('uuid', $request->destination_uuid)->first();
+ if (! $destination) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'destination_uuid' => 'Provided destination_uuid does not belong to the specified server.',
+ ],
+ ], 422);
+ }
+ }
if ($type === 'public') {
$validationRules = [
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
@@ -2936,7 +2947,7 @@ public function envs(Request $request)
)]
public function update_env_by_uuid(Request $request)
{
- $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime'];
+ $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -2966,6 +2977,7 @@ public function update_env_by_uuid(Request $request)
'is_shown_once' => 'boolean',
'is_runtime' => 'boolean',
'is_buildtime' => 'boolean',
+ 'comment' => 'string|nullable|max:256',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -3007,6 +3019,9 @@ public function update_env_by_uuid(Request $request)
if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
$env->is_buildtime = $request->is_buildtime;
}
+ if ($request->has('comment') && $env->comment != $request->comment) {
+ $env->comment = $request->comment;
+ }
$env->save();
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
@@ -3037,6 +3052,9 @@ public function update_env_by_uuid(Request $request)
if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
$env->is_buildtime = $request->is_buildtime;
}
+ if ($request->has('comment') && $env->comment != $request->comment) {
+ $env->comment = $request->comment;
+ }
$env->save();
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
@@ -3329,7 +3347,7 @@ public function create_bulk_envs(Request $request)
)]
public function create_env(Request $request)
{
- $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime'];
+ $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -3354,6 +3372,7 @@ public function create_env(Request $request)
'is_shown_once' => 'boolean',
'is_runtime' => 'boolean',
'is_buildtime' => 'boolean',
+ 'comment' => 'string|nullable|max:256',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -3389,6 +3408,7 @@ public function create_env(Request $request)
'is_shown_once' => $request->is_shown_once ?? false,
'is_runtime' => $request->is_runtime ?? true,
'is_buildtime' => $request->is_buildtime ?? true,
+ 'comment' => $request->comment ?? null,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
@@ -3413,6 +3433,7 @@ public function create_env(Request $request)
'is_shown_once' => $request->is_shown_once ?? false,
'is_runtime' => $request->is_runtime ?? true,
'is_buildtime' => $request->is_buildtime ?? true,
+ 'comment' => $request->comment ?? null,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 29c6b854a..892457925 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -11,6 +11,7 @@
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server as ModelsServer;
+use App\Rules\ValidServerIp;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Stringable;
@@ -472,10 +473,10 @@ public function create_server(Request $request)
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|nullable',
- 'ip' => 'string|required',
- 'port' => 'integer|nullable',
+ 'ip' => ['string', 'required', new ValidServerIp],
+ 'port' => 'integer|nullable|between:1,65535',
'private_key_uuid' => 'string|required',
- 'user' => 'string|nullable',
+ 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'],
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
@@ -637,10 +638,10 @@ public function update_server(Request $request)
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|nullable',
'description' => 'string|nullable',
- 'ip' => 'string|nullable',
- 'port' => 'integer|nullable',
+ 'ip' => ['string', 'nullable', new ValidServerIp],
+ 'port' => 'integer|nullable|between:1,65535',
'private_key_uuid' => 'string|nullable',
- 'user' => 'string|nullable',
+ 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'],
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index ee4d84f10..98b35f63e 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -377,6 +377,17 @@ public function create_service(Request $request)
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
+ if ($destinations->count() > 1 && $request->has('destination_uuid')) {
+ $destination = $destinations->where('uuid', $request->destination_uuid)->first();
+ if (! $destination) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'destination_uuid' => 'Provided destination_uuid does not belong to the specified server.',
+ ],
+ ], 422);
+ }
+ }
$services = get_service_templates();
$serviceKeys = $services->keys();
if ($serviceKeys->contains($request->type)) {
@@ -543,6 +554,17 @@ public function create_service(Request $request)
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
+ if ($destinations->count() > 1 && $request->has('destination_uuid')) {
+ $destination = $destinations->where('uuid', $request->destination_uuid)->first();
+ if (! $destination) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'destination_uuid' => 'Provided destination_uuid does not belong to the specified server.',
+ ],
+ ], 422);
+ }
+ }
if (! isBase64Encoded($request->docker_compose_raw)) {
return response()->json([
'message' => 'Validation failed.',
@@ -1184,6 +1206,7 @@ public function update_env_by_uuid(Request $request)
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
+ 'comment' => 'string|nullable|max:256',
]);
if ($validator->fails()) {
@@ -1199,7 +1222,19 @@ public function update_env_by_uuid(Request $request)
return response()->json(['message' => 'Environment variable not found.'], 404);
}
- $env->fill($request->all());
+ $env->value = $request->value;
+ if ($request->has('is_literal')) {
+ $env->is_literal = $request->is_literal;
+ }
+ if ($request->has('is_multiline')) {
+ $env->is_multiline = $request->is_multiline;
+ }
+ if ($request->has('is_shown_once')) {
+ $env->is_shown_once = $request->is_shown_once;
+ }
+ if ($request->has('comment')) {
+ $env->comment = $request->comment;
+ }
$env->save();
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
@@ -1425,6 +1460,7 @@ public function create_env(Request $request)
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
+ 'comment' => 'string|nullable|max:256',
]);
if ($validator->fails()) {
@@ -1442,7 +1478,14 @@ public function create_env(Request $request)
], 409);
}
- $env = $service->environment_variables()->create($request->all());
+ $env = $service->environment_variables()->create([
+ 'key' => $key,
+ 'value' => $request->value,
+ 'is_literal' => $request->is_literal ?? false,
+ 'is_multiline' => $request->is_multiline ?? false,
+ 'is_shown_once' => $request->is_shown_once ?? false,
+ 'comment' => $request->comment ?? null,
+ ]);
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
}
diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php
index f0b9d67f2..5fca583d9 100644
--- a/app/Http/Middleware/TrustHosts.php
+++ b/app/Http/Middleware/TrustHosts.php
@@ -91,6 +91,13 @@ public function hosts(): array
// Trust all subdomains of APP_URL as fallback
$trustedHosts[] = $this->allSubdomainsOfApplicationUrl();
+ // Always trust loopback addresses so local access works even when FQDN is configured
+ foreach (['localhost', '127.0.0.1', '[::1]'] as $localHost) {
+ if (! in_array($localHost, $trustedHosts, true)) {
+ $trustedHosts[] = $localHost;
+ }
+ }
+
return array_filter($trustedHosts);
}
}
diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php
index 559dd2fc3..a4764047b 100644
--- a/app/Http/Middleware/TrustProxies.php
+++ b/app/Http/Middleware/TrustProxies.php
@@ -25,4 +25,26 @@ class TrustProxies extends Middleware
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
+
+ /**
+ * Handle the request.
+ *
+ * Wraps $next so that after proxy headers are resolved (X-Forwarded-Proto processed),
+ * the Secure cookie flag is auto-enabled when the request is over HTTPS.
+ * This ensures session cookies are correctly marked Secure when behind an HTTPS
+ * reverse proxy (Cloudflare Tunnel, nginx, etc.) even when SESSION_SECURE_COOKIE
+ * is not explicitly set in .env.
+ */
+ public function handle($request, \Closure $next)
+ {
+ return parent::handle($request, function ($request) use ($next) {
+ // At this point proxy headers have been applied to the request,
+ // so $request->secure() correctly reflects the actual protocol.
+ if ($request->secure() && config('session.secure') === null) {
+ config(['session.secure' => true]);
+ }
+
+ return $next($request);
+ });
+ }
}
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index aa82c6dad..06e94fc93 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -38,7 +38,7 @@ public function handle()
$server->forceDisableServer();
$this->team->notify(new ForceDisabled($server));
});
- } elseif ($number_of_servers_to_disable === 0) {
+ } elseif ($number_of_servers_to_disable <= 0) {
$servers->each(function ($server) {
if ($server->isForceDisabled()) {
$server->forceEnableServer();
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 18bb237af..634a012c0 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -63,10 +63,16 @@ public function submit()
]);
$variables = parseEnvFormatToArray($this->envFile);
- foreach ($variables as $key => $variable) {
+ foreach ($variables as $key => $data) {
+ // Extract value and comment from parsed data
+ // Handle both array format ['value' => ..., 'comment' => ...] and plain string values
+ $value = is_array($data) ? ($data['value'] ?? '') : $data;
+ $comment = is_array($data) ? ($data['comment'] ?? null) : null;
+
EnvironmentVariable::create([
'key' => $key,
- 'value' => $variable,
+ 'value' => $value,
+ 'comment' => $comment,
'is_preview' => false,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index fa65e8bd2..73d5393b0 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -31,6 +31,8 @@ class Add extends Component
public bool $is_buildtime = true;
+ public ?string $comment = null;
+
public array $problematicVariables = [];
protected $listeners = ['clearAddEnv' => 'clear'];
@@ -42,6 +44,7 @@ class Add extends Component
'is_literal' => 'required|boolean',
'is_runtime' => 'required|boolean',
'is_buildtime' => 'required|boolean',
+ 'comment' => 'nullable|string|max:256',
];
protected $validationAttributes = [
@@ -51,6 +54,7 @@ class Add extends Component
'is_literal' => 'literal',
'is_runtime' => 'runtime',
'is_buildtime' => 'buildtime',
+ 'comment' => 'comment',
];
public function mount()
@@ -136,6 +140,7 @@ public function submit()
'is_runtime' => $this->is_runtime,
'is_buildtime' => $this->is_buildtime,
'is_preview' => $this->is_preview,
+ 'comment' => $this->comment,
]);
$this->clear();
}
@@ -148,5 +153,6 @@ public function clear()
$this->is_literal = false;
$this->is_runtime = true;
$this->is_buildtime = true;
+ $this->comment = null;
}
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 55e388b78..f250a860b 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -89,6 +89,62 @@ public function getEnvironmentVariablesPreviewProperty()
return $query->get();
}
+ public function getHardcodedEnvironmentVariablesProperty()
+ {
+ return $this->getHardcodedVariables(false);
+ }
+
+ public function getHardcodedEnvironmentVariablesPreviewProperty()
+ {
+ return $this->getHardcodedVariables(true);
+ }
+
+ protected function getHardcodedVariables(bool $isPreview)
+ {
+ // Only for services and docker-compose applications
+ if ($this->resource->type() !== 'service' &&
+ ($this->resourceClass !== 'App\Models\Application' ||
+ ($this->resourceClass === 'App\Models\Application' && $this->resource->build_pack !== 'dockercompose'))) {
+ return collect([]);
+ }
+
+ $dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
+
+ if (blank($dockerComposeRaw)) {
+ return collect([]);
+ }
+
+ // Extract all hard-coded variables
+ $hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw);
+
+ // Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*)
+ $hardcodedVars = $hardcodedVars->filter(function ($var) {
+ $key = $var['key'];
+
+ return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
+ });
+
+ // Filter out variables that exist in database (user has overridden/managed them)
+ // For preview, check against preview variables; for production, check against production variables
+ if ($isPreview) {
+ $managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
+ } else {
+ $managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
+ }
+
+ $hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
+ return ! in_array($var['key'], $managedKeys);
+ });
+
+ // Apply sorting based on is_env_sorting_enabled
+ if ($this->is_env_sorting_enabled) {
+ $hardcodedVars = $hardcodedVars->sortBy('key')->values();
+ }
+ // Otherwise keep order from docker-compose file
+
+ return $hardcodedVars;
+ }
+
public function getDevView()
{
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
@@ -240,6 +296,7 @@ private function createEnvironmentVariable($data)
$environment->is_runtime = $data['is_runtime'] ?? true;
$environment->is_buildtime = $data['is_buildtime'] ?? true;
$environment->is_preview = $data['is_preview'] ?? false;
+ $environment->comment = $data['comment'] ?? null;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
@@ -280,18 +337,37 @@ private function deleteRemovedVariables($isPreview, $variables)
private function updateOrCreateVariables($isPreview, $variables)
{
$count = 0;
- foreach ($variables as $key => $value) {
+ foreach ($variables as $key => $data) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
continue;
}
+
+ // Extract value and comment from parsed data
+ // Handle both array format ['value' => ..., 'comment' => ...] and plain string values
+ $value = is_array($data) ? ($data['value'] ?? '') : $data;
+ $comment = is_array($data) ? ($data['comment'] ?? null) : null;
+
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
$found = $this->resource->$method()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
- // Only count as a change if the value actually changed
+ $changed = false;
+
+ // Update value if it changed
if ($found->value !== $value) {
$found->value = $value;
+ $changed = true;
+ }
+
+ // Only update comment from inline comment if one is provided (overwrites existing)
+ // If $comment is null, don't touch existing comment field to preserve it
+ if ($comment !== null && $found->comment !== $comment) {
+ $found->comment = $comment;
+ $changed = true;
+ }
+
+ if ($changed) {
$found->save();
$count++;
}
@@ -300,6 +376,7 @@ private function updateOrCreateVariables($isPreview, $variables)
$environment = new EnvironmentVariable;
$environment->key = $key;
$environment->value = $value;
+ $environment->comment = $comment; // Set comment from inline comment
$environment->is_multiline = false;
$environment->is_preview = $isPreview;
$environment->resourceable_id = $this->resource->id;
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index 2030f631e..2a18be13c 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -24,6 +24,8 @@ class Show extends Component
public bool $isLocked = false;
+ public bool $isMagicVariable = false;
+
public bool $isSharedVariable = false;
public string $type;
@@ -34,6 +36,8 @@ class Show extends Component
public ?string $real_value = null;
+ public ?string $comment = null;
+
public bool $is_shared = false;
public bool $is_multiline = false;
@@ -63,6 +67,7 @@ class Show extends Component
protected $rules = [
'key' => 'required|string',
'value' => 'nullable',
+ 'comment' => 'nullable|string|max:256',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
@@ -104,6 +109,7 @@ public function syncData(bool $toModel = false)
$this->validate([
'key' => 'required|string',
'value' => 'nullable',
+ 'comment' => 'nullable|string|max:256',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
@@ -118,6 +124,7 @@ public function syncData(bool $toModel = false)
}
$this->env->key = $this->key;
$this->env->value = $this->value;
+ $this->env->comment = $this->comment;
$this->env->is_multiline = $this->is_multiline;
$this->env->is_literal = $this->is_literal;
$this->env->is_shown_once = $this->is_shown_once;
@@ -125,6 +132,7 @@ public function syncData(bool $toModel = false)
} else {
$this->key = $this->env->key;
$this->value = $this->env->value;
+ $this->comment = $this->env->comment;
$this->is_multiline = $this->env->is_multiline;
$this->is_literal = $this->env->is_literal;
$this->is_shown_once = $this->env->is_shown_once;
@@ -140,9 +148,13 @@ public function syncData(bool $toModel = false)
public function checkEnvs()
{
$this->isDisabled = false;
+ $this->isMagicVariable = false;
+
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
$this->isDisabled = true;
+ $this->isMagicVariable = true;
}
+
if ($this->env->is_shown_once) {
$this->isLocked = true;
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/ShowHardcoded.php b/app/Livewire/Project/Shared/EnvironmentVariable/ShowHardcoded.php
new file mode 100644
index 000000000..3a49ce124
--- /dev/null
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/ShowHardcoded.php
@@ -0,0 +1,31 @@
+key = $this->env['key'];
+ $this->value = $this->env['value'] ?? null;
+ $this->comment = $this->env['comment'] ?? null;
+ $this->serviceName = $this->env['service_name'] ?? null;
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.environment-variable.show-hardcoded');
+ }
+}
diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php
index eecdfb4d0..51c6a06ee 100644
--- a/app/Livewire/Server/New/ByIp.php
+++ b/app/Livewire/Server/New/ByIp.php
@@ -5,6 +5,7 @@
use App\Enums\ProxyTypes;
use App\Models\Server;
use App\Models\Team;
+use App\Rules\ValidServerIp;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
@@ -55,8 +56,8 @@ protected function rules(): array
'new_private_key_value' => 'nullable|string',
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
- 'ip' => 'required|string',
- 'user' => 'required|string',
+ 'ip' => ['required', 'string', new ValidServerIp],
+ 'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9_-]+$/'],
'port' => 'required|integer|between:1,65535',
'is_build_server' => 'required|boolean',
];
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 83c63a81c..edc17004c 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -7,6 +7,7 @@
use App\Events\ServerReachabilityChanged;
use App\Models\CloudProviderToken;
use App\Models\Server;
+use App\Rules\ValidServerIp;
use App\Services\HetznerService;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -106,9 +107,9 @@ protected function rules(): array
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
- 'ip' => 'required',
- 'user' => 'required',
- 'port' => 'required',
+ 'ip' => ['required', new ValidServerIp],
+ 'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
+ 'port' => 'required|integer|between:1,65535',
'validationLogs' => 'nullable',
'wildcardDomain' => 'nullable|url',
'isReachable' => 'required',
diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php
index 16361ce79..ad478273f 100644
--- a/app/Livewire/Settings/Advanced.php
+++ b/app/Livewire/Settings/Advanced.php
@@ -95,7 +95,9 @@ public function submit()
// Check if it's valid CIDR notation
if (str_contains($entry, '/')) {
[$ip, $mask] = explode('/', $entry);
- if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= 32) {
+ $isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+ $maxMask = $isIpv6 ? 128 : 32;
+ if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= $maxMask) {
return $entry;
}
$invalidEntries[] = $entry;
@@ -111,7 +113,7 @@ public function submit()
$invalidEntries[] = $entry;
return null;
- })->filter()->unique();
+ })->filter()->values()->all();
if (! empty($invalidEntries)) {
$this->dispatch('error', 'Invalid IP addresses or subnets: '.implode(', ', $invalidEntries));
@@ -119,13 +121,15 @@ public function submit()
return;
}
- if ($validEntries->isEmpty()) {
+ if (empty($validEntries)) {
$this->dispatch('error', 'No valid IP addresses or subnets provided');
return;
}
- $this->allowed_ips = $validEntries->implode(',');
+ $validEntries = deduplicateAllowlist($validEntries);
+
+ $this->allowed_ips = implode(',', $validEntries);
}
$this->instantSave();
diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php
index 0bdc1503f..e1b230218 100644
--- a/app/Livewire/SharedVariables/Environment/Show.php
+++ b/app/Livewire/SharedVariables/Environment/Show.php
@@ -40,6 +40,7 @@ public function saveKey($data)
'value' => $data['value'],
'is_multiline' => $data['is_multiline'],
'is_literal' => $data['is_literal'],
+ 'comment' => $data['comment'] ?? null,
'type' => 'environment',
'team_id' => currentTeam()->id,
]);
diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php
index b205ea1ec..1f304b543 100644
--- a/app/Livewire/SharedVariables/Project/Show.php
+++ b/app/Livewire/SharedVariables/Project/Show.php
@@ -33,6 +33,7 @@ public function saveKey($data)
'value' => $data['value'],
'is_multiline' => $data['is_multiline'],
'is_literal' => $data['is_literal'],
+ 'comment' => $data['comment'] ?? null,
'type' => 'project',
'team_id' => currentTeam()->id,
]);
diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php
index e420686f0..75fd415e1 100644
--- a/app/Livewire/SharedVariables/Team/Index.php
+++ b/app/Livewire/SharedVariables/Team/Index.php
@@ -33,6 +33,7 @@ public function saveKey($data)
'value' => $data['value'],
'is_multiline' => $data['is_multiline'],
'is_literal' => $data['is_literal'],
+ 'comment' => $data['comment'] ?? null,
'type' => 'team',
'team_id' => currentTeam()->id,
]);
diff --git a/app/Livewire/Subscription/Actions.php b/app/Livewire/Subscription/Actions.php
index 1388d3244..2d5392240 100644
--- a/app/Livewire/Subscription/Actions.php
+++ b/app/Livewire/Subscription/Actions.php
@@ -2,21 +2,204 @@
namespace App\Livewire\Subscription;
+use App\Actions\Stripe\CancelSubscriptionAtPeriodEnd;
+use App\Actions\Stripe\RefundSubscription;
+use App\Actions\Stripe\ResumeSubscription;
+use App\Actions\Stripe\UpdateSubscriptionQuantity;
use App\Models\Team;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
+use Stripe\StripeClient;
class Actions extends Component
{
public $server_limits = 0;
- public function mount()
+ public int $quantity = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
+
+ public int $minServerLimit = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
+
+ public int $maxServerLimit = UpdateSubscriptionQuantity::MAX_SERVER_LIMIT;
+
+ public ?array $pricePreview = null;
+
+ public bool $isRefundEligible = false;
+
+ public int $refundDaysRemaining = 0;
+
+ public bool $refundCheckLoading = true;
+
+ public bool $refundAlreadyUsed = false;
+
+ public function mount(): void
{
$this->server_limits = Team::serverLimit();
+ $this->quantity = (int) $this->server_limits;
}
- public function stripeCustomerPortal()
+ public function loadPricePreview(int $quantity): void
+ {
+ $this->quantity = $quantity;
+ $result = (new UpdateSubscriptionQuantity)->fetchPricePreview(currentTeam(), $quantity);
+ $this->pricePreview = $result['success'] ? $result['preview'] : null;
+ }
+
+ // Password validation is intentionally skipped for quantity updates.
+ // Unlike refunds/cancellations, changing the server limit is a
+ // non-destructive, reversible billing adjustment (prorated by Stripe).
+ public function updateQuantity(string $password = ''): bool
+ {
+ if ($this->quantity < UpdateSubscriptionQuantity::MIN_SERVER_LIMIT) {
+ $this->dispatch('error', 'Minimum server limit is '.UpdateSubscriptionQuantity::MIN_SERVER_LIMIT.'.');
+ $this->quantity = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
+
+ return true;
+ }
+
+ if ($this->quantity === (int) $this->server_limits) {
+ return true;
+ }
+
+ $result = (new UpdateSubscriptionQuantity)->execute(currentTeam(), $this->quantity);
+
+ if ($result['success']) {
+ $this->server_limits = $this->quantity;
+ $this->pricePreview = null;
+ $this->dispatch('success', 'Server limit updated to '.$this->quantity.'.');
+
+ return true;
+ }
+
+ $this->dispatch('error', $result['error'] ?? 'Failed to update server limit.');
+ $this->quantity = (int) $this->server_limits;
+
+ return true;
+ }
+
+ public function loadRefundEligibility(): void
+ {
+ $this->checkRefundEligibility();
+ $this->refundCheckLoading = false;
+ }
+
+ public function stripeCustomerPortal(): void
{
$session = getStripeCustomerPortalSession(currentTeam());
redirect($session->url);
}
+
+ public function refundSubscription(string $password): bool|string
+ {
+ if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
+ return 'Invalid password.';
+ }
+
+ $result = (new RefundSubscription)->execute(currentTeam());
+
+ if ($result['success']) {
+ $this->dispatch('success', 'Subscription refunded successfully.');
+ $this->redirect(route('subscription.index'), navigate: true);
+
+ return true;
+ }
+
+ $this->dispatch('error', 'Something went wrong with the refund. Please contact us.');
+
+ return true;
+ }
+
+ public function cancelImmediately(string $password): bool|string
+ {
+ if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
+ return 'Invalid password.';
+ }
+
+ $team = currentTeam();
+ $subscription = $team->subscription;
+
+ if (! $subscription?->stripe_subscription_id) {
+ $this->dispatch('error', 'Something went wrong with the cancellation. Please contact us.');
+
+ return true;
+ }
+
+ try {
+ $stripe = new StripeClient(config('subscription.stripe_api_key'));
+ $stripe->subscriptions->cancel($subscription->stripe_subscription_id);
+
+ $subscription->update([
+ 'stripe_cancel_at_period_end' => false,
+ 'stripe_invoice_paid' => false,
+ 'stripe_trial_already_ended' => false,
+ 'stripe_past_due' => false,
+ 'stripe_feedback' => 'Cancelled immediately by user',
+ 'stripe_comment' => 'Subscription cancelled immediately by user at '.now()->toDateTimeString(),
+ ]);
+
+ $team->subscriptionEnded();
+
+ \Log::info("Subscription {$subscription->stripe_subscription_id} cancelled immediately for team {$team->name}");
+
+ $this->dispatch('success', 'Subscription cancelled successfully.');
+ $this->redirect(route('subscription.index'), navigate: true);
+
+ return true;
+ } catch (\Exception $e) {
+ \Log::error("Immediate cancellation error for team {$team->id}: ".$e->getMessage());
+
+ $this->dispatch('error', 'Something went wrong with the cancellation. Please contact us.');
+
+ return true;
+ }
+ }
+
+ public function cancelAtPeriodEnd(string $password): bool|string
+ {
+ if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
+ return 'Invalid password.';
+ }
+
+ $result = (new CancelSubscriptionAtPeriodEnd)->execute(currentTeam());
+
+ if ($result['success']) {
+ $this->dispatch('success', 'Subscription will be cancelled at the end of the billing period.');
+
+ return true;
+ }
+
+ $this->dispatch('error', 'Something went wrong with the cancellation. Please contact us.');
+
+ return true;
+ }
+
+ public function resumeSubscription(): bool
+ {
+ $result = (new ResumeSubscription)->execute(currentTeam());
+
+ if ($result['success']) {
+ $this->dispatch('success', 'Subscription resumed successfully.');
+
+ return true;
+ }
+
+ $this->dispatch('error', 'Something went wrong resuming the subscription. Please contact us.');
+
+ return true;
+ }
+
+ private function checkRefundEligibility(): void
+ {
+ if (! isCloud() || ! currentTeam()->subscription?->stripe_subscription_id) {
+ return;
+ }
+
+ try {
+ $this->refundAlreadyUsed = currentTeam()->subscription?->stripe_refunded_at !== null;
+ $result = (new RefundSubscription)->checkEligibility(currentTeam());
+ $this->isRefundEligible = $result['eligible'];
+ $this->refundDaysRemaining = $result['days_remaining'];
+ } catch (\Exception $e) {
+ \Log::warning('Refund eligibility check failed: '.$e->getMessage());
+ }
+ }
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index a239c34db..a4f51780e 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1087,19 +1087,24 @@ public function dirOnServer()
return application_configuration_dir()."/{$this->uuid}";
}
- public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
+ public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
$escapedBaseDir = escapeshellarg($baseDir);
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;
- if ($this->git_commit_sha !== 'HEAD') {
+ // Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha.
+ // Invalid refs will cause the git checkout/fetch command to fail on the remote server.
+ $commitToUse = $commit ?? $this->git_commit_sha;
+
+ if ($commitToUse !== 'HEAD') {
+ $escapedCommit = escapeshellarg($commitToUse);
// If shallow clone is enabled and we need a specific commit,
// we need to fetch that specific commit with depth=1
if ($isShallowCloneEnabled) {
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$this->git_commit_sha} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
} else {
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
}
}
if ($this->settings->is_git_submodules_enabled) {
@@ -1287,7 +1292,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
if (! $only_checkout) {
- $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
+ $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
@@ -1308,7 +1313,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$fullRepoUrl = $repoUrl;
}
if (! $only_checkout) {
- $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
+ $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false, commit: $commit);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
@@ -1347,7 +1352,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
} else {
- $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
+ $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit);
}
if ($exec_in_docker) {
$commands = collect([
@@ -1405,7 +1410,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$fullRepoUrl = $customRepository;
$escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
- $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
+ $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit);
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
@@ -1961,7 +1966,6 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
}
}
-
public function getLimits(): array
{
return [
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index c2ad9d2cb..d4e614e6e 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -4,6 +4,7 @@
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -21,6 +22,7 @@
class Environment extends BaseModel
{
use ClearsGlobalSearchCache;
+ use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 4f1e277e4..0a004f765 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -24,6 +24,7 @@
'key' => ['type' => 'string'],
'value' => ['type' => 'string'],
'real_value' => ['type' => 'string'],
+ 'comment' => ['type' => 'string', 'nullable' => true],
'version' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
@@ -31,7 +32,30 @@
)]
class EnvironmentVariable extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ // Core identification
+ 'key',
+ 'value',
+ 'comment',
+
+ // Polymorphic relationship
+ 'resourceable_type',
+ 'resourceable_id',
+
+ // Boolean flags
+ 'is_preview',
+ 'is_multiline',
+ 'is_literal',
+ 'is_runtime',
+ 'is_buildtime',
+ 'is_shown_once',
+ 'is_shared',
+ 'is_required',
+
+ // Metadata
+ 'version',
+ 'order',
+ ];
protected $casts = [
'key' => 'string',
@@ -67,6 +91,7 @@ protected static function booted()
'is_literal' => $environment_variable->is_literal ?? false,
'is_runtime' => $environment_variable->is_runtime ?? false,
'is_buildtime' => $environment_variable->is_buildtime ?? false,
+ 'comment' => $environment_variable->comment,
'resourceable_type' => Application::class,
'resourceable_id' => $environment_variable->resourceable_id,
'is_preview' => true,
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 181951f14..ed1b415c1 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -4,6 +4,7 @@
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
@@ -20,6 +21,7 @@
class Project extends BaseModel
{
use ClearsGlobalSearchCache;
+ use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 272638a81..e771ce31e 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use OpenApi\Attributes as OA;
@@ -25,6 +26,7 @@
)]
class ScheduledTask extends BaseModel
{
+ use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 49d9c3289..5099a9fec 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -913,6 +913,9 @@ public function port(): Attribute
return Attribute::make(
get: function ($value) {
return (int) preg_replace('/[^0-9]/', '', $value);
+ },
+ set: function ($value) {
+ return (int) preg_replace('/[^0-9]/', '', (string) $value);
}
);
}
@@ -922,6 +925,9 @@ public function user(): Attribute
return Attribute::make(
get: function ($value) {
return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
+ },
+ set: function ($value) {
+ return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
}
);
}
@@ -931,6 +937,9 @@ public function ip(): Attribute
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
+ },
+ set: function ($value) {
+ return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
}
);
}
diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php
index 7956f006a..9bd42c328 100644
--- a/app/Models/SharedEnvironmentVariable.php
+++ b/app/Models/SharedEnvironmentVariable.php
@@ -6,7 +6,23 @@
class SharedEnvironmentVariable extends Model
{
- protected $guarded = [];
+ protected $fillable = [
+ // Core identification
+ 'key',
+ 'value',
+ 'comment',
+
+ // Type and relationships
+ 'type',
+ 'team_id',
+ 'project_id',
+ 'environment_id',
+
+ // Boolean flags
+ 'is_multiline',
+ 'is_literal',
+ 'is_shown_once',
+ ];
protected $casts = [
'key' => 'string',
diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php
index 62ef68434..0407c2255 100644
--- a/app/Models/StandaloneDocker.php
+++ b/app/Models/StandaloneDocker.php
@@ -4,9 +4,11 @@
use App\Jobs\ConnectProxyToNetworksJob;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
class StandaloneDocker extends BaseModel
{
+ use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php
index 1bd84a664..69d7cbf0d 100644
--- a/app/Models/Subscription.php
+++ b/app/Models/Subscription.php
@@ -8,11 +8,32 @@ class Subscription extends Model
{
protected $guarded = [];
+ protected function casts(): array
+ {
+ return [
+ 'stripe_refunded_at' => 'datetime',
+ ];
+ }
+
public function team()
{
return $this->belongsTo(Team::class);
}
+ public function billingInterval(): string
+ {
+ if ($this->stripe_plan_id) {
+ $configKey = collect(config('subscription'))
+ ->search($this->stripe_plan_id);
+
+ if ($configKey && str($configKey)->contains('yearly')) {
+ return 'yearly';
+ }
+ }
+
+ return 'monthly';
+ }
+
public function type()
{
if (isStripe()) {
diff --git a/app/Rules/ValidIpOrCidr.php b/app/Rules/ValidIpOrCidr.php
index e172ffd1a..bd0bd2296 100644
--- a/app/Rules/ValidIpOrCidr.php
+++ b/app/Rules/ValidIpOrCidr.php
@@ -45,7 +45,10 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
[$ip, $mask] = $parts;
- if (! filter_var($ip, FILTER_VALIDATE_IP) || ! is_numeric($mask) || $mask < 0 || $mask > 32) {
+ $isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+ $maxMask = $isIpv6 ? 128 : 32;
+
+ if (! filter_var($ip, FILTER_VALIDATE_IP) || ! is_numeric($mask) || $mask < 0 || $mask > $maxMask) {
$invalidEntries[] = $entry;
}
} else {
diff --git a/app/Rules/ValidServerIp.php b/app/Rules/ValidServerIp.php
new file mode 100644
index 000000000..270ff1c34
--- /dev/null
+++ b/app/Rules/ValidServerIp.php
@@ -0,0 +1,40 @@
+validate($attribute, $trimmed, function () use (&$failed) {
+ $failed = true;
+ });
+
+ if ($failed) {
+ $fail('The :attribute must be a valid IPv4 address, IPv6 address, or hostname.');
+ }
+ }
+}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 77e8b7b07..7b74392cf 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -1006,6 +1006,7 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
'--ip' => 'ip',
+ '--ip6' => 'ip6',
'--shm-size' => 'shm_size',
'--gpus' => 'gpus',
'--hostname' => 'hostname',
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index 53060d28f..fa40857ac 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -998,53 +998,139 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
} else {
if ($value->startsWith('$')) {
$isRequired = false;
- if ($value->contains(':-')) {
- $value = replaceVariables($value);
- $key = $value->before(':');
- $value = $value->after(':-');
- } elseif ($value->contains('-')) {
- $value = replaceVariables($value);
- $key = $value->before('-');
- $value = $value->after('-');
- } elseif ($value->contains(':?')) {
- $value = replaceVariables($value);
+ // Extract variable content between ${...} using balanced brace matching
+ $result = extractBalancedBraceContent($value->value(), 0);
- $key = $value->before(':');
- $value = $value->after(':?');
- $isRequired = true;
- } elseif ($value->contains('?')) {
- $value = replaceVariables($value);
+ if ($result !== null) {
+ $content = $result['content'];
+ $split = splitOnOperatorOutsideNested($content);
- $key = $value->before('?');
- $value = $value->after('?');
- $isRequired = true;
- }
- if ($originalValue->value() === $value->value()) {
- // This means the variable does not have a default value, so it needs to be created in Coolify
- $parsedKeyValue = replaceVariables($value);
+ if ($split !== null) {
+ // Has default value syntax (:-, -, :?, or ?)
+ $varName = $split['variable'];
+ $operator = $split['operator'];
+ $defaultValue = $split['default'];
+ $isRequired = str_contains($operator, '?');
+
+ // Create the primary variable with its default (only if it doesn't exist)
+ $envVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $varName,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $defaultValue,
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ ]);
+
+ // Add the variable to the environment so it will be shown in the deployable compose file
+ $environment[$varName] = $envVar->value;
+
+ // Recursively process nested variables in default value
+ if (str_contains($defaultValue, '${')) {
+ $searchPos = 0;
+ $nestedResult = extractBalancedBraceContent($defaultValue, $searchPos);
+ while ($nestedResult !== null) {
+ $nestedContent = $nestedResult['content'];
+ $nestedSplit = splitOnOperatorOutsideNested($nestedContent);
+
+ // Determine the nested variable name
+ $nestedVarName = $nestedSplit !== null ? $nestedSplit['variable'] : $nestedContent;
+
+ // Skip SERVICE_URL_* and SERVICE_FQDN_* variables - they are handled by magic variable system
+ $isMagicVariable = str_starts_with($nestedVarName, 'SERVICE_URL_') || str_starts_with($nestedVarName, 'SERVICE_FQDN_');
+
+ if (! $isMagicVariable) {
+ if ($nestedSplit !== null) {
+ $nestedEnvVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $nestedSplit['variable'],
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $nestedSplit['default'],
+ 'is_preview' => false,
+ ]);
+ $environment[$nestedSplit['variable']] = $nestedEnvVar->value;
+ } else {
+ $nestedEnvVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $nestedContent,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ ]);
+ $environment[$nestedContent] = $nestedEnvVar->value;
+ }
+ }
+
+ $searchPos = $nestedResult['end'] + 1;
+ if ($searchPos >= strlen($defaultValue)) {
+ break;
+ }
+ $nestedResult = extractBalancedBraceContent($defaultValue, $searchPos);
+ }
+ }
+ } else {
+ // Simple variable reference without default
+ $parsedKeyValue = replaceVariables($value);
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $content,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ ]);
+ // Add the variable to the environment
+ $environment[$content] = $value;
+ }
+ } else {
+ // Fallback to old behavior for malformed input (backward compatibility)
+ if ($value->contains(':-')) {
+ $value = replaceVariables($value);
+ $key = $value->before(':');
+ $value = $value->after(':-');
+ } elseif ($value->contains('-')) {
+ $value = replaceVariables($value);
+ $key = $value->before('-');
+ $value = $value->after('-');
+ } elseif ($value->contains(':?')) {
+ $value = replaceVariables($value);
+ $key = $value->before(':');
+ $value = $value->after(':?');
+ $isRequired = true;
+ } elseif ($value->contains('?')) {
+ $value = replaceVariables($value);
+ $key = $value->before('?');
+ $value = $value->after('?');
+ $isRequired = true;
+ }
+ if ($originalValue->value() === $value->value()) {
+ // This means the variable does not have a default value
+ $parsedKeyValue = replaceVariables($value);
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $parsedKeyValue,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ ]);
+ $environment[$parsedKeyValue->value()] = $value;
+
+ continue;
+ }
$resource->environment_variables()->firstOrCreate([
- 'key' => $parsedKeyValue,
+ 'key' => $key,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
+ 'value' => $value,
'is_preview' => false,
'is_required' => $isRequired,
]);
- // Add the variable to the environment so it will be shown in the deployable compose file
- $environment[$parsedKeyValue->value()] = $value;
-
- continue;
}
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key,
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $value,
- 'is_preview' => false,
- 'is_required' => $isRequired,
- ]);
}
}
}
@@ -1411,6 +1497,9 @@ function serviceParser(Service $resource): Collection
return collect([]);
}
+ // Extract inline comments from raw YAML before Symfony parser discards them
+ $envComments = extractYamlEnvironmentComments($compose);
+
$server = data_get($resource, 'server');
$allServices = get_service_templates();
@@ -1694,51 +1783,60 @@ function serviceParser(Service $resource): Collection
}
// ALWAYS create BOTH base SERVICE_URL and SERVICE_FQDN pairs (without port)
+ $fqdnKey = "SERVICE_FQDN_{$serviceName}";
$resource->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_FQDN_{$serviceName}",
+ 'key' => $fqdnKey,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $fqdnValueForEnv,
'is_preview' => false,
+ 'comment' => $envComments[$fqdnKey] ?? null,
]);
+ $urlKey = "SERVICE_URL_{$serviceName}";
$resource->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_URL_{$serviceName}",
+ 'key' => $urlKey,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $url,
'is_preview' => false,
+ 'comment' => $envComments[$urlKey] ?? null,
]);
// For port-specific variables, ALSO create port-specific pairs
// If template variable has port, create both URL and FQDN with port suffix
if ($parsed['has_port'] && $port) {
+ $fqdnPortKey = "SERVICE_FQDN_{$serviceName}_{$port}";
$resource->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_FQDN_{$serviceName}_{$port}",
+ 'key' => $fqdnPortKey,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $fqdnValueForEnvWithPort,
'is_preview' => false,
+ 'comment' => $envComments[$fqdnPortKey] ?? null,
]);
+ $urlPortKey = "SERVICE_URL_{$serviceName}_{$port}";
$resource->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_URL_{$serviceName}_{$port}",
+ 'key' => $urlPortKey,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $urlWithPort,
'is_preview' => false,
+ 'comment' => $envComments[$urlPortKey] ?? null,
]);
}
}
}
$allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
if ($magicEnvironments->count() > 0) {
- foreach ($magicEnvironments as $key => $value) {
- $key = str($key);
+ foreach ($magicEnvironments as $magicKey => $value) {
+ $originalMagicKey = $magicKey; // Preserve original key for comment lookup
+ $key = str($magicKey);
$value = replaceVariables($value);
$command = parseCommandFromMagicEnvVariable($key);
if ($command->value() === 'FQDN') {
@@ -1762,18 +1860,33 @@ function serviceParser(Service $resource): Collection
$serviceExists->fqdn = $url;
$serviceExists->save();
}
- $resource->environment_variables()->firstOrCreate([
+ // Create FQDN variable
+ $resource->environment_variables()->updateOrCreate([
'key' => $key->value(),
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $fqdn,
'is_preview' => false,
+ 'comment' => $envComments[$originalMagicKey] ?? null,
+ ]);
+
+ // Also create the paired SERVICE_URL_* variable
+ $urlKey = 'SERVICE_URL_'.strtoupper($fqdnFor);
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => $urlKey,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $url,
+ 'is_preview' => false,
+ 'comment' => $envComments[$urlKey] ?? null,
]);
} elseif ($command->value() === 'URL') {
$urlFor = $key->after('SERVICE_URL_')->lower()->value();
$url = generateUrl(server: $server, random: str($urlFor)->replace('_', '-')->value()."-$uuid");
+ $fqdn = generateFqdn(server: $server, random: str($urlFor)->replace('_', '-')->value()."-$uuid", parserVersion: $resource->compose_parsing_version);
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
// Also check if a port-suffixed version exists (e.g., SERVICE_URL_DASHBOARD_6791)
@@ -1790,24 +1903,39 @@ function serviceParser(Service $resource): Collection
$serviceExists->fqdn = $url;
$serviceExists->save();
}
- $resource->environment_variables()->firstOrCreate([
+ // Create URL variable
+ $resource->environment_variables()->updateOrCreate([
'key' => $key->value(),
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $url,
'is_preview' => false,
+ 'comment' => $envComments[$originalMagicKey] ?? null,
+ ]);
+
+ // Also create the paired SERVICE_FQDN_* variable
+ $fqdnKey = 'SERVICE_FQDN_'.strtoupper($urlFor);
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => $fqdnKey,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdn,
+ 'is_preview' => false,
+ 'comment' => $envComments[$fqdnKey] ?? null,
]);
} else {
$value = generateEnvValue($command, $resource);
- $resource->environment_variables()->firstOrCreate([
+ $resource->environment_variables()->updateOrCreate([
'key' => $key->value(),
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $value,
'is_preview' => false,
+ 'comment' => $envComments[$originalMagicKey] ?? null,
]);
}
}
@@ -2163,18 +2291,20 @@ function serviceParser(Service $resource): Collection
return ! str($value)->startsWith('SERVICE_');
});
foreach ($normalEnvironments as $key => $value) {
+ $originalKey = $key; // Preserve original key for comment lookup
$key = str($key);
$value = str($value);
$originalValue = $value;
$parsedValue = replaceVariables($value);
if ($parsedValue->startsWith('SERVICE_')) {
- $resource->environment_variables()->firstOrCreate([
+ $resource->environment_variables()->updateOrCreate([
'key' => $key,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $value,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
continue;
@@ -2184,64 +2314,161 @@ function serviceParser(Service $resource): Collection
}
if ($key->value() === $parsedValue->value()) {
$value = null;
- $resource->environment_variables()->firstOrCreate([
+ $resource->environment_variables()->updateOrCreate([
'key' => $key,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $value,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
} else {
if ($value->startsWith('$')) {
$isRequired = false;
- if ($value->contains(':-')) {
- $value = replaceVariables($value);
- $key = $value->before(':');
- $value = $value->after(':-');
- } elseif ($value->contains('-')) {
- $value = replaceVariables($value);
- $key = $value->before('-');
- $value = $value->after('-');
- } elseif ($value->contains(':?')) {
- $value = replaceVariables($value);
+ // Extract variable content between ${...} using balanced brace matching
+ $result = extractBalancedBraceContent($value->value(), 0);
- $key = $value->before(':');
- $value = $value->after(':?');
- $isRequired = true;
- } elseif ($value->contains('?')) {
- $value = replaceVariables($value);
+ if ($result !== null) {
+ $content = $result['content'];
+ $split = splitOnOperatorOutsideNested($content);
- $key = $value->before('?');
- $value = $value->after('?');
- $isRequired = true;
- }
- if ($originalValue->value() === $value->value()) {
- // This means the variable does not have a default value, so it needs to be created in Coolify
- $parsedKeyValue = replaceVariables($value);
- $resource->environment_variables()->firstOrCreate([
- 'key' => $parsedKeyValue,
+ if ($split !== null) {
+ // Has default value syntax (:-, -, :?, or ?)
+ $varName = $split['variable'];
+ $operator = $split['operator'];
+ $defaultValue = $split['default'];
+ $isRequired = str_contains($operator, '?');
+
+ // Create the primary variable with its default (only if it doesn't exist)
+ // Use firstOrCreate instead of updateOrCreate to avoid overwriting user edits
+ $envVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $varName,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $defaultValue,
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ 'comment' => $envComments[$originalKey] ?? null,
+ ]);
+
+ // Add the variable to the environment so it will be shown in the deployable compose file
+ $environment[$varName] = $envVar->value;
+
+ // Recursively process nested variables in default value
+ if (str_contains($defaultValue, '${')) {
+ // Extract and create nested variables
+ $searchPos = 0;
+ $nestedResult = extractBalancedBraceContent($defaultValue, $searchPos);
+ while ($nestedResult !== null) {
+ $nestedContent = $nestedResult['content'];
+ $nestedSplit = splitOnOperatorOutsideNested($nestedContent);
+
+ // Determine the nested variable name
+ $nestedVarName = $nestedSplit !== null ? $nestedSplit['variable'] : $nestedContent;
+
+ // Skip SERVICE_URL_* and SERVICE_FQDN_* variables - they are handled by magic variable system
+ $isMagicVariable = str_starts_with($nestedVarName, 'SERVICE_URL_') || str_starts_with($nestedVarName, 'SERVICE_FQDN_');
+
+ if (! $isMagicVariable) {
+ if ($nestedSplit !== null) {
+ // Create nested variable with its default (only if it doesn't exist)
+ $nestedEnvVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $nestedSplit['variable'],
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $nestedSplit['default'],
+ 'is_preview' => false,
+ ]);
+ // Add nested variable to environment
+ $environment[$nestedSplit['variable']] = $nestedEnvVar->value;
+ } else {
+ // Simple nested variable without default (only if it doesn't exist)
+ $nestedEnvVar = $resource->environment_variables()->firstOrCreate([
+ 'key' => $nestedContent,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ ]);
+ // Add nested variable to environment
+ $environment[$nestedContent] = $nestedEnvVar->value;
+ }
+ }
+
+ // Look for more nested variables
+ $searchPos = $nestedResult['end'] + 1;
+ if ($searchPos >= strlen($defaultValue)) {
+ break;
+ }
+ $nestedResult = extractBalancedBraceContent($defaultValue, $searchPos);
+ }
+ }
+ } else {
+ // Simple variable reference without default
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => $content,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ 'comment' => $envComments[$originalKey] ?? null,
+ ]);
+ }
+ } else {
+ // Fallback to old behavior for malformed input (backward compatibility)
+ if ($value->contains(':-')) {
+ $value = replaceVariables($value);
+ $key = $value->before(':');
+ $value = $value->after(':-');
+ } elseif ($value->contains('-')) {
+ $value = replaceVariables($value);
+ $key = $value->before('-');
+ $value = $value->after('-');
+ } elseif ($value->contains(':?')) {
+ $value = replaceVariables($value);
+ $key = $value->before(':');
+ $value = $value->after(':?');
+ $isRequired = true;
+ } elseif ($value->contains('?')) {
+ $value = replaceVariables($value);
+ $key = $value->before('?');
+ $value = $value->after('?');
+ $isRequired = true;
+ }
+
+ if ($originalValue->value() === $value->value()) {
+ // This means the variable does not have a default value
+ $parsedKeyValue = replaceVariables($value);
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => $parsedKeyValue,
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'is_preview' => false,
+ 'is_required' => $isRequired,
+ 'comment' => $envComments[$originalKey] ?? null,
+ ]);
+ // Add the variable to the environment so it will be shown in the deployable compose file
+ $environment[$parsedKeyValue->value()] = $value;
+
+ continue;
+ }
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => $key,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
+ 'value' => $value,
'is_preview' => false,
'is_required' => $isRequired,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
- // Add the variable to the environment so it will be shown in the deployable compose file
- $environment[$parsedKeyValue->value()] = $value;
-
- continue;
}
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key,
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $value,
- 'is_preview' => false,
- 'is_required' => $isRequired,
- ]);
}
}
}
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index 3d2b61b86..bd741b76e 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -17,9 +17,118 @@ function collectRegex(string $name)
{
return "/{$name}\w+/";
}
+
+/**
+ * Extract content between balanced braces, handling nested braces properly.
+ *
+ * @param string $str The string to search
+ * @param int $startPos Position to start searching from
+ * @return array|null Array with 'content', 'start', and 'end' keys, or null if no balanced braces found
+ */
+function extractBalancedBraceContent(string $str, int $startPos = 0): ?array
+{
+ // Find opening brace
+ if ($startPos >= strlen($str)) {
+ return null;
+ }
+ $openPos = strpos($str, '{', $startPos);
+ if ($openPos === false) {
+ return null;
+ }
+
+ // Track depth to find matching closing brace
+ $depth = 1;
+ $pos = $openPos + 1;
+ $len = strlen($str);
+
+ while ($pos < $len && $depth > 0) {
+ if ($str[$pos] === '{') {
+ $depth++;
+ } elseif ($str[$pos] === '}') {
+ $depth--;
+ }
+ $pos++;
+ }
+
+ if ($depth !== 0) {
+ // Unbalanced braces
+ return null;
+ }
+
+ return [
+ 'content' => substr($str, $openPos + 1, $pos - $openPos - 2),
+ 'start' => $openPos,
+ 'end' => $pos - 1,
+ ];
+}
+
+/**
+ * Split variable expression on operators (:-, -, :?, ?) while respecting nested braces.
+ *
+ * @param string $content The content to split (without outer ${...})
+ * @return array|null Array with 'variable', 'operator', and 'default' keys, or null if no operator found
+ */
+function splitOnOperatorOutsideNested(string $content): ?array
+{
+ $operators = [':-', '-', ':?', '?'];
+ $depth = 0;
+ $len = strlen($content);
+
+ for ($i = 0; $i < $len; $i++) {
+ if ($content[$i] === '{') {
+ $depth++;
+ } elseif ($content[$i] === '}') {
+ $depth--;
+ } elseif ($depth === 0) {
+ // Check for operators only at depth 0 (outside nested braces)
+ foreach ($operators as $op) {
+ if (substr($content, $i, strlen($op)) === $op) {
+ return [
+ 'variable' => substr($content, 0, $i),
+ 'operator' => $op,
+ 'default' => substr($content, $i + strlen($op)),
+ ];
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
function replaceVariables(string $variable): Stringable
{
- return str($variable)->before('}')->replaceFirst('$', '')->replaceFirst('{', '');
+ // Handle ${VAR} syntax with proper brace matching
+ $str = str($variable);
+
+ // Handle ${VAR} format
+ if ($str->startsWith('${')) {
+ $result = extractBalancedBraceContent($variable, 0);
+ if ($result !== null) {
+ return str($result['content']);
+ }
+
+ // Fallback to old behavior for malformed input
+ return $str->before('}')->replaceFirst('$', '')->replaceFirst('{', '');
+ }
+
+ // Handle {VAR} format (from regex capture group without $)
+ if ($str->startsWith('{') && $str->endsWith('}')) {
+ return str(substr($variable, 1, -1));
+ }
+
+ // Handle {VAR format (from regex capture group, may be truncated)
+ if ($str->startsWith('{')) {
+ $result = extractBalancedBraceContent('$'.$variable, 0);
+ if ($result !== null) {
+ return str($result['content']);
+ }
+
+ // Fallback: remove { and get content before }
+ return $str->replaceFirst('{', '')->before('}');
+ }
+
+ return $str;
}
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 4372ff955..3e993dbf3 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -448,19 +448,286 @@ function parseEnvFormatToArray($env_file_contents)
$equals_pos = strpos($line, '=');
if ($equals_pos !== false) {
$key = substr($line, 0, $equals_pos);
- $value = substr($line, $equals_pos + 1);
- if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
- $value = substr($value, 1, -1);
- } elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
- $value = substr($value, 1, -1);
+ $value_and_comment = substr($line, $equals_pos + 1);
+ $comment = null;
+ $remainder = '';
+
+ // Check if value starts with quotes
+ $firstChar = $value_and_comment[0] ?? '';
+ $isDoubleQuoted = $firstChar === '"';
+ $isSingleQuoted = $firstChar === "'";
+
+ if ($isDoubleQuoted) {
+ // Find the closing double quote
+ $closingPos = strpos($value_and_comment, '"', 1);
+ if ($closingPos !== false) {
+ // Extract quoted value and remove quotes
+ $value = substr($value_and_comment, 1, $closingPos - 1);
+ // Everything after closing quote (including comments)
+ $remainder = substr($value_and_comment, $closingPos + 1);
+ } else {
+ // No closing quote - treat as unquoted
+ $value = substr($value_and_comment, 1);
+ }
+ } elseif ($isSingleQuoted) {
+ // Find the closing single quote
+ $closingPos = strpos($value_and_comment, "'", 1);
+ if ($closingPos !== false) {
+ // Extract quoted value and remove quotes
+ $value = substr($value_and_comment, 1, $closingPos - 1);
+ // Everything after closing quote (including comments)
+ $remainder = substr($value_and_comment, $closingPos + 1);
+ } else {
+ // No closing quote - treat as unquoted
+ $value = substr($value_and_comment, 1);
+ }
+ } else {
+ // Unquoted value - strip inline comments
+ // Only treat # as comment if preceded by whitespace
+ if (preg_match('/\s+#/', $value_and_comment, $matches, PREG_OFFSET_CAPTURE)) {
+ // Found whitespace followed by #, extract comment
+ $remainder = substr($value_and_comment, $matches[0][1]);
+ $value = substr($value_and_comment, 0, $matches[0][1]);
+ $value = rtrim($value);
+ } else {
+ $value = $value_and_comment;
+ }
}
- $env_array[$key] = $value;
+
+ // Extract comment from remainder (if any)
+ if ($remainder !== '') {
+ // Look for # in remainder
+ $hashPos = strpos($remainder, '#');
+ if ($hashPos !== false) {
+ // Extract everything after the # and trim
+ $comment = substr($remainder, $hashPos + 1);
+ $comment = trim($comment);
+ // Set to null if empty after trimming
+ if ($comment === '') {
+ $comment = null;
+ }
+ }
+ }
+
+ $env_array[$key] = [
+ 'value' => $value,
+ 'comment' => $comment,
+ ];
}
}
return $env_array;
}
+/**
+ * Extract inline comments from environment variables in raw docker-compose YAML.
+ *
+ * Parses raw docker-compose YAML to extract inline comments from environment sections.
+ * Standard YAML parsers discard comments, so this pre-processes the raw text.
+ *
+ * Handles both formats:
+ * - Map format: `KEY: "value" # comment` or `KEY: value # comment`
+ * - Array format: `- KEY=value # comment`
+ *
+ * @param string $rawYaml The raw docker-compose.yml content
+ * @return array Map of environment variable keys to their inline comments
+ */
+function extractYamlEnvironmentComments(string $rawYaml): array
+{
+ $comments = [];
+ $lines = explode("\n", $rawYaml);
+ $inEnvironmentBlock = false;
+ $environmentIndent = 0;
+
+ foreach ($lines as $line) {
+ // Skip empty lines
+ if (trim($line) === '') {
+ continue;
+ }
+
+ // Calculate current line's indentation (number of leading spaces)
+ $currentIndent = strlen($line) - strlen(ltrim($line));
+
+ // Check if this line starts an environment block
+ if (preg_match('/^(\s*)environment\s*:\s*$/', $line, $matches)) {
+ $inEnvironmentBlock = true;
+ $environmentIndent = strlen($matches[1]);
+
+ continue;
+ }
+
+ // Check if this line starts an environment block with inline content (rare but possible)
+ if (preg_match('/^(\s*)environment\s*:\s*\{/', $line)) {
+ // Inline object format - not supported for comment extraction
+ continue;
+ }
+
+ // If we're in an environment block, check if we've exited it
+ if ($inEnvironmentBlock) {
+ // If we hit a line with same or less indentation that's not empty, we've left the block
+ // Unless it's a continuation of the environment block
+ $trimmedLine = ltrim($line);
+
+ // Check if this is a new top-level key (same indent as 'environment:' or less)
+ if ($currentIndent <= $environmentIndent && ! str_starts_with($trimmedLine, '-') && ! str_starts_with($trimmedLine, '#')) {
+ // Check if it looks like a YAML key (contains : not inside quotes)
+ if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*\s*:/', $trimmedLine)) {
+ $inEnvironmentBlock = false;
+
+ continue;
+ }
+ }
+
+ // Skip comment-only lines
+ if (str_starts_with($trimmedLine, '#')) {
+ continue;
+ }
+
+ // Try to extract environment variable and comment from this line
+ $extracted = extractEnvVarCommentFromYamlLine($trimmedLine);
+ if ($extracted !== null && $extracted['comment'] !== null) {
+ $comments[$extracted['key']] = $extracted['comment'];
+ }
+ }
+ }
+
+ return $comments;
+}
+
+/**
+ * Extract environment variable key and inline comment from a single YAML line.
+ *
+ * @param string $line A trimmed line from the environment section
+ * @return array|null Array with 'key' and 'comment', or null if not an env var line
+ */
+function extractEnvVarCommentFromYamlLine(string $line): ?array
+{
+ $key = null;
+ $comment = null;
+
+ // Handle array format: `- KEY=value # comment` or `- KEY # comment`
+ if (str_starts_with($line, '-')) {
+ $content = ltrim(substr($line, 1));
+
+ // Check for KEY=value format
+ if (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)/', $content, $keyMatch)) {
+ $key = $keyMatch[1];
+ // Find comment - need to handle quoted values
+ $comment = extractCommentAfterValue($content);
+ }
+ }
+ // Handle map format: `KEY: "value" # comment` or `KEY: value # comment`
+ elseif (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s*:/', $line, $keyMatch)) {
+ $key = $keyMatch[1];
+ // Get everything after the key and colon
+ $afterKey = substr($line, strlen($keyMatch[0]));
+ $comment = extractCommentAfterValue($afterKey);
+ }
+
+ if ($key === null) {
+ return null;
+ }
+
+ return [
+ 'key' => $key,
+ 'comment' => $comment,
+ ];
+}
+
+/**
+ * Extract inline comment from a value portion of a YAML line.
+ *
+ * Handles quoted values (where # inside quotes is not a comment).
+ *
+ * @param string $valueAndComment The value portion (may include comment)
+ * @return string|null The comment text, or null if no comment
+ */
+function extractCommentAfterValue(string $valueAndComment): ?string
+{
+ $valueAndComment = ltrim($valueAndComment);
+
+ if ($valueAndComment === '') {
+ return null;
+ }
+
+ $firstChar = $valueAndComment[0] ?? '';
+
+ // Handle case where value is empty and line starts directly with comment
+ // e.g., `KEY: # comment` becomes `# comment` after ltrim
+ if ($firstChar === '#') {
+ $comment = trim(substr($valueAndComment, 1));
+
+ return $comment !== '' ? $comment : null;
+ }
+
+ // Handle double-quoted value
+ if ($firstChar === '"') {
+ // Find closing quote (handle escaped quotes)
+ $pos = 1;
+ $len = strlen($valueAndComment);
+ while ($pos < $len) {
+ if ($valueAndComment[$pos] === '\\' && $pos + 1 < $len) {
+ $pos += 2; // Skip escaped character
+
+ continue;
+ }
+ if ($valueAndComment[$pos] === '"') {
+ // Found closing quote
+ $remainder = substr($valueAndComment, $pos + 1);
+
+ return extractCommentFromRemainder($remainder);
+ }
+ $pos++;
+ }
+
+ // No closing quote found
+ return null;
+ }
+
+ // Handle single-quoted value
+ if ($firstChar === "'") {
+ // Find closing quote (single quotes don't have escapes in YAML)
+ $closingPos = strpos($valueAndComment, "'", 1);
+ if ($closingPos !== false) {
+ $remainder = substr($valueAndComment, $closingPos + 1);
+
+ return extractCommentFromRemainder($remainder);
+ }
+
+ // No closing quote found
+ return null;
+ }
+
+ // Unquoted value - find # that's preceded by whitespace
+ // Be careful not to match # at the start of a value like color codes
+ if (preg_match('/\s+#\s*(.*)$/', $valueAndComment, $matches)) {
+ $comment = trim($matches[1]);
+
+ return $comment !== '' ? $comment : null;
+ }
+
+ return null;
+}
+
+/**
+ * Extract comment from the remainder of a line after a quoted value.
+ *
+ * @param string $remainder Text after the closing quote
+ * @return string|null The comment text, or null if no comment
+ */
+function extractCommentFromRemainder(string $remainder): ?string
+{
+ // Look for # in remainder
+ $hashPos = strpos($remainder, '#');
+ if ($hashPos !== false) {
+ $comment = trim(substr($remainder, $hashPos + 1));
+
+ return $comment !== '' ? $comment : null;
+ }
+
+ return null;
+}
+
function data_get_str($data, $key, $default = null): Stringable
{
$str = data_get($data, $key, $default) ?? $default;
@@ -1149,24 +1416,48 @@ function checkIPAgainstAllowlist($ip, $allowlist)
}
$mask = (int) $mask;
+ $isIpv6Subnet = filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+ $maxMask = $isIpv6Subnet ? 128 : 32;
- // Validate mask
- if ($mask < 0 || $mask > 32) {
+ // Validate mask for address family
+ if ($mask < 0 || $mask > $maxMask) {
continue;
}
- // Calculate network addresses
- $ip_long = ip2long($ip);
- $subnet_long = ip2long($subnet);
+ if ($isIpv6Subnet) {
+ // IPv6 CIDR matching using binary string comparison
+ $ipBin = inet_pton($ip);
+ $subnetBin = inet_pton($subnet);
- if ($ip_long === false || $subnet_long === false) {
- continue;
- }
+ if ($ipBin === false || $subnetBin === false) {
+ continue;
+ }
- $mask_long = ~((1 << (32 - $mask)) - 1);
+ // Build a 128-bit mask from $mask prefix bits
+ $maskBin = str_repeat("\xff", (int) ($mask / 8));
+ $remainder = $mask % 8;
+ if ($remainder > 0) {
+ $maskBin .= chr(0xFF & (0xFF << (8 - $remainder)));
+ }
+ $maskBin = str_pad($maskBin, 16, "\x00");
- if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
- return true;
+ if (($ipBin & $maskBin) === ($subnetBin & $maskBin)) {
+ return true;
+ }
+ } else {
+ // IPv4 CIDR matching
+ $ip_long = ip2long($ip);
+ $subnet_long = ip2long($subnet);
+
+ if ($ip_long === false || $subnet_long === false) {
+ continue;
+ }
+
+ $mask_long = ~((1 << (32 - $mask)) - 1);
+
+ if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
+ return true;
+ }
}
} else {
// Special case: 0.0.0.0 means allow all
@@ -1184,6 +1475,67 @@ function checkIPAgainstAllowlist($ip, $allowlist)
return false;
}
+function deduplicateAllowlist(array $entries): array
+{
+ if (count($entries) <= 1) {
+ return array_values($entries);
+ }
+
+ // Normalize each entry into [original, ip, mask]
+ $parsed = [];
+ foreach ($entries as $entry) {
+ $entry = trim($entry);
+ if (empty($entry)) {
+ continue;
+ }
+
+ if ($entry === '0.0.0.0') {
+ // Special case: bare 0.0.0.0 means "allow all" — treat as /0
+ $parsed[] = ['original' => $entry, 'ip' => '0.0.0.0', 'mask' => 0];
+ } elseif (str_contains($entry, '/')) {
+ [$ip, $mask] = explode('/', $entry);
+ $parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => (int) $mask];
+ } else {
+ $ip = $entry;
+ $isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+ $parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => $isIpv6 ? 128 : 32];
+ }
+ }
+
+ $count = count($parsed);
+ $redundant = array_fill(0, $count, false);
+
+ for ($i = 0; $i < $count; $i++) {
+ if ($redundant[$i]) {
+ continue;
+ }
+
+ for ($j = 0; $j < $count; $j++) {
+ if ($i === $j || $redundant[$j]) {
+ continue;
+ }
+
+ // Entry $j is redundant if its mask is narrower/equal (>=) than $i's mask
+ // AND $j's network IP falls within $i's CIDR range
+ if ($parsed[$j]['mask'] >= $parsed[$i]['mask']) {
+ $cidr = $parsed[$i]['ip'].'/'.$parsed[$i]['mask'];
+ if (checkIPAgainstAllowlist($parsed[$j]['ip'], [$cidr])) {
+ $redundant[$j] = true;
+ }
+ }
+ }
+ }
+
+ $result = [];
+ for ($i = 0; $i < $count; $i++) {
+ if (! $redundant[$i]) {
+ $result[] = $parsed[$i]['original'];
+ }
+ }
+
+ return $result;
+}
+
function get_public_ips()
{
try {
@@ -1317,6 +1669,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
{
if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
+ // Extract inline comments from raw YAML before Symfony parser discards them
+ $envComments = extractYamlEnvironmentComments($resource->docker_compose_raw);
+
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
@@ -1348,7 +1703,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
$topLevelVolumes = collect($tempTopLevelVolumes);
}
- $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices) {
+ $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices, $envComments) {
// Workarounds for beta users.
if ($serviceName === 'registry') {
$tempServiceName = 'docker-registry';
@@ -1694,6 +2049,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$key = str($variableName);
$value = str($variable);
}
+ // Preserve original key for comment lookup before $key might be reassigned
+ $originalKey = $key->value();
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -1747,6 +2104,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
}
// Caddy needs exact port in some cases.
@@ -1826,6 +2184,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
}
if (! $isDatabase) {
@@ -1864,6 +2223,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
}
}
@@ -1902,6 +2262,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
+ 'comment' => $envComments[$originalKey] ?? null,
]);
}
}
@@ -3447,6 +3808,58 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon
return true;
}
+/**
+ * Extract hard-coded environment variables from docker-compose YAML.
+ *
+ * @param string $dockerComposeRaw Raw YAML content
+ * @return \Illuminate\Support\Collection Collection of arrays with: key, value, comment, service_name
+ */
+function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): \Illuminate\Support\Collection
+{
+ if (blank($dockerComposeRaw)) {
+ return collect([]);
+ }
+
+ try {
+ $yaml = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+ } catch (\Exception $e) {
+ // Malformed YAML - return empty collection
+ return collect([]);
+ }
+
+ $services = data_get($yaml, 'services', []);
+ if (empty($services)) {
+ return collect([]);
+ }
+
+ // Extract inline comments from raw YAML
+ $envComments = extractYamlEnvironmentComments($dockerComposeRaw);
+
+ $hardcodedVars = collect([]);
+
+ foreach ($services as $serviceName => $service) {
+ $environment = collect(data_get($service, 'environment', []));
+
+ if ($environment->isEmpty()) {
+ continue;
+ }
+
+ // Convert environment variables to key-value format
+ $environment = convertToKeyValueCollection($environment);
+
+ foreach ($environment as $key => $value) {
+ $hardcodedVars->push([
+ 'key' => $key,
+ 'value' => $value,
+ 'comment' => $envComments[$key] ?? null,
+ 'service_name' => $serviceName,
+ ]);
+ }
+ }
+
+ return $hardcodedVars;
+}
+
/**
* Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
* This preserves the visual shape of the data better than simple averaging.
diff --git a/composer.json b/composer.json
index fc71dea8f..d4fb1eb8e 100644
--- a/composer.json
+++ b/composer.json
@@ -54,7 +54,7 @@
"stevebauman/purify": "^6.3.1",
"stripe/stripe-php": "^16.6.0",
"symfony/yaml": "^7.4.1",
- "visus/cuid2": "^4.1.0",
+ "visus/cuid2": "^6.0.0",
"yosymfony/toml": "^1.0.4",
"zircote/swagger-php": "^5.8.0"
},
diff --git a/composer.lock b/composer.lock
index 7c1e000e5..4d890881a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "21d43f41d2f2e275403e77ccc66ec553",
+ "content-hash": "19bb661d294e5cf623e68830604e4f60",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.369.26",
+ "version": "3.371.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94"
+ "reference": "d300ec1c861e52dc8f17ca3d75dc754da949f065"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ad0916c6595d98f9052f60e1d7204f4740369e94",
- "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d300ec1c861e52dc8f17ca3d75dc754da949f065",
+ "reference": "d300ec1c861e52dc8f17ca3d75dc754da949f065",
"shasum": ""
},
"require": {
@@ -135,11 +135,11 @@
"authors": [
{
"name": "Amazon Web Services",
- "homepage": "http://aws.amazon.com"
+ "homepage": "https://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
- "homepage": "http://aws.amazon.com/sdkforphp",
+ "homepage": "https://aws.amazon.com/sdk-for-php",
"keywords": [
"amazon",
"aws",
@@ -153,9 +153,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.369.26"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.371.3"
},
- "time": "2026-02-03T19:16:42+00:00"
+ "time": "2026-02-27T19:05:40+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -214,16 +214,16 @@
},
{
"name": "brick/math",
- "version": "0.14.5",
+ "version": "0.14.8",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "618a8077b3c326045e10d5788ed713b341fcfe40"
+ "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/618a8077b3c326045e10d5788ed713b341fcfe40",
- "reference": "618a8077b3c326045e10d5788ed713b341fcfe40",
+ "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629",
+ "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629",
"shasum": ""
},
"require": {
@@ -262,7 +262,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.14.5"
+ "source": "https://github.com/brick/math/tree/0.14.8"
},
"funding": [
{
@@ -270,7 +270,7 @@
"type": "github"
}
],
- "time": "2026-02-03T18:06:51+00:00"
+ "time": "2026-02-10T14:33:43+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -522,16 +522,16 @@
},
{
"name": "doctrine/dbal",
- "version": "4.4.1",
+ "version": "4.4.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c"
+ "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c",
- "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/476f7f0fa6ea4aa5364926db7fabdf6049075722",
+ "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722",
"shasum": ""
},
"require": {
@@ -547,9 +547,9 @@
"phpstan/phpstan": "2.1.30",
"phpstan/phpstan-phpunit": "2.0.7",
"phpstan/phpstan-strict-rules": "^2",
- "phpunit/phpunit": "11.5.23",
- "slevomat/coding-standard": "8.24.0",
- "squizlabs/php_codesniffer": "4.0.0",
+ "phpunit/phpunit": "11.5.50",
+ "slevomat/coding-standard": "8.27.1",
+ "squizlabs/php_codesniffer": "4.0.1",
"symfony/cache": "^6.3.8|^7.0|^8.0",
"symfony/console": "^5.4|^6.3|^7.0|^8.0"
},
@@ -608,7 +608,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/4.4.1"
+ "source": "https://github.com/doctrine/dbal/tree/4.4.2"
},
"funding": [
{
@@ -624,33 +624,33 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T10:11:03+00:00"
+ "time": "2026-02-26T12:12:19+00:00"
},
{
"name": "doctrine/deprecations",
- "version": "1.1.5",
+ "version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
- "phpunit/phpunit": "<=7.5 || >=13"
+ "phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
- "doctrine/coding-standard": "^9 || ^12 || ^13",
- "phpstan/phpstan": "1.4.10 || 2.1.11",
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
@@ -670,9 +670,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
- "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
- "time": "2025-04-07T20:06:18+00:00"
+ "time": "2026-02-07T07:09:04+00:00"
},
{
"name": "doctrine/inflector",
@@ -1035,16 +1035,16 @@
},
{
"name": "firebase/php-jwt",
- "version": "v7.0.2",
+ "version": "v7.0.3",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65"
+ "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
+ "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
"shasum": ""
},
"require": {
@@ -1092,9 +1092,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
- "source": "https://github.com/firebase/php-jwt/tree/v7.0.2"
+ "source": "https://github.com/firebase/php-jwt/tree/v7.0.3"
},
- "time": "2025-12-16T22:17:28+00:00"
+ "time": "2026-02-25T22:16:40+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -1702,28 +1702,28 @@
},
{
"name": "laravel/fortify",
- "version": "v1.34.0",
+ "version": "v1.35.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
- "reference": "c322715f2786210a722ed56966f7c9877b653b25"
+ "reference": "24c5bb81ea4787e0865c4a62f054ed7d1cb7a093"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/fortify/zipball/c322715f2786210a722ed56966f7c9877b653b25",
- "reference": "c322715f2786210a722ed56966f7c9877b653b25",
+ "url": "https://api.github.com/repos/laravel/fortify/zipball/24c5bb81ea4787e0865c4a62f054ed7d1cb7a093",
+ "reference": "24c5bb81ea4787e0865c4a62f054ed7d1cb7a093",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^3.0",
"ext-json": "*",
- "illuminate/support": "^10.0|^11.0|^12.0",
+ "illuminate/console": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
- "pragmarx/google2fa": "^9.0",
- "symfony/console": "^6.0|^7.0"
+ "pragmarx/google2fa": "^9.0"
},
"require-dev": {
- "orchestra/testbench": "^8.36|^9.15|^10.8",
+ "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
@@ -1761,20 +1761,20 @@
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
- "time": "2026-01-26T10:23:19+00:00"
+ "time": "2026-02-24T14:00:44+00:00"
},
{
"name": "laravel/framework",
- "version": "v12.49.0",
+ "version": "v12.53.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5"
+ "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/4bde4530545111d8bdd1de6f545fa8824039fcb5",
- "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/f57f035c0d34503d9ff30be76159bb35a003cd1f",
+ "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f",
"shasum": ""
},
"require": {
@@ -1983,40 +1983,41 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-01-28T03:40:49+00:00"
+ "time": "2026-02-24T14:35:15+00:00"
},
{
"name": "laravel/horizon",
- "version": "v5.43.0",
+ "version": "v5.45.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
- "reference": "2a04285ba83915511afbe987cbfedafdc27fd2de"
+ "reference": "7126ddf27fe9750c43ab0b567085dee3917d0510"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/horizon/zipball/2a04285ba83915511afbe987cbfedafdc27fd2de",
- "reference": "2a04285ba83915511afbe987cbfedafdc27fd2de",
+ "url": "https://api.github.com/repos/laravel/horizon/zipball/7126ddf27fe9750c43ab0b567085dee3917d0510",
+ "reference": "7126ddf27fe9750c43ab0b567085dee3917d0510",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcntl": "*",
"ext-posix": "*",
- "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0",
- "illuminate/queue": "^9.21|^10.0|^11.0|^12.0",
- "illuminate/support": "^9.21|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
+ "laravel/sentinel": "^1.0",
"nesbot/carbon": "^2.17|^3.0",
"php": "^8.0",
"ramsey/uuid": "^4.0",
- "symfony/console": "^6.0|^7.0",
- "symfony/error-handler": "^6.0|^7.0",
+ "symfony/console": "^6.0|^7.0|^8.0",
+ "symfony/error-handler": "^6.0|^7.0|^8.0",
"symfony/polyfill-php83": "^1.28",
- "symfony/process": "^6.0|^7.0"
+ "symfony/process": "^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
- "orchestra/testbench": "^7.55|^8.36|^9.15|^10.8",
+ "orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0",
"phpstan/phpstan": "^1.10|^2.0",
"predis/predis": "^1.1|^2.0|^3.0"
},
@@ -2060,43 +2061,44 @@
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
- "source": "https://github.com/laravel/horizon/tree/v5.43.0"
+ "source": "https://github.com/laravel/horizon/tree/v5.45.0"
},
- "time": "2026-01-15T15:10:56+00:00"
+ "time": "2026-02-21T14:20:09+00:00"
},
{
"name": "laravel/pail",
- "version": "v1.2.4",
+ "version": "v1.2.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/pail.git",
- "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30"
+ "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30",
- "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf",
+ "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
- "illuminate/console": "^10.24|^11.0|^12.0",
- "illuminate/contracts": "^10.24|^11.0|^12.0",
- "illuminate/log": "^10.24|^11.0|^12.0",
- "illuminate/process": "^10.24|^11.0|^12.0",
- "illuminate/support": "^10.24|^11.0|^12.0",
+ "illuminate/console": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/log": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/process": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.24|^11.0|^12.0|^13.0",
"nunomaduro/termwind": "^1.15|^2.0",
"php": "^8.2",
- "symfony/console": "^6.0|^7.0"
+ "symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
- "laravel/framework": "^10.24|^11.0|^12.0",
+ "laravel/framework": "^10.24|^11.0|^12.0|^13.0",
"laravel/pint": "^1.13",
- "orchestra/testbench-core": "^8.13|^9.17|^10.8",
+ "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0",
"pestphp/pest": "^2.20|^3.0|^4.0",
"pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0",
"phpstan/phpstan": "^1.12.27",
- "symfony/var-dumper": "^6.3|^7.0"
+ "symfony/var-dumper": "^6.3|^7.0|^8.0",
+ "symfony/yaml": "^6.3|^7.0|^8.0"
},
"type": "library",
"extra": {
@@ -2141,34 +2143,34 @@
"issues": "https://github.com/laravel/pail/issues",
"source": "https://github.com/laravel/pail"
},
- "time": "2025-11-20T16:29:35+00:00"
+ "time": "2026-02-09T13:44:54+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.3.11",
+ "version": "v0.3.13",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217"
+ "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217",
- "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d",
+ "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2.2",
"ext-mbstring": "*",
"php": "^8.1",
- "symfony/console": "^6.2|^7.0"
+ "symfony/console": "^6.2|^7.0|^8.0"
},
"conflict": {
"illuminate/console": ">=10.17.0 <10.25.0",
"laravel/framework": ">=10.17.0 <10.25.0"
},
"require-dev": {
- "illuminate/collections": "^10.0|^11.0|^12.0",
+ "illuminate/collections": "^10.0|^11.0|^12.0|^13.0",
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.4|^4.0",
"phpstan/phpstan": "^1.12.28",
@@ -2198,36 +2200,36 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.11"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.13"
},
- "time": "2026-01-27T02:55:06+00:00"
+ "time": "2026-02-06T12:17:10+00:00"
},
{
"name": "laravel/sanctum",
- "version": "v4.3.0",
+ "version": "v4.3.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
- "reference": "c978c82b2b8ab685468a7ca35224497d541b775a"
+ "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sanctum/zipball/c978c82b2b8ab685468a7ca35224497d541b775a",
- "reference": "c978c82b2b8ab685468a7ca35224497d541b775a",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
+ "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
"shasum": ""
},
"require": {
"ext-json": "*",
- "illuminate/console": "^11.0|^12.0",
- "illuminate/contracts": "^11.0|^12.0",
- "illuminate/database": "^11.0|^12.0",
- "illuminate/support": "^11.0|^12.0",
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "illuminate/database": "^11.0|^12.0|^13.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
- "symfony/console": "^7.0"
+ "symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
- "orchestra/testbench": "^9.15|^10.8",
+ "orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
@@ -2263,31 +2265,90 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
- "time": "2026-01-22T22:27:01+00:00"
+ "time": "2026-02-07T17:19:31+00:00"
},
{
- "name": "laravel/serializable-closure",
- "version": "v2.0.8",
+ "name": "laravel/sentinel",
+ "version": "v1.0.1",
"source": {
"type": "git",
- "url": "https://github.com/laravel/serializable-closure.git",
- "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b"
+ "url": "https://github.com/laravel/sentinel.git",
+ "reference": "7a98db53e0d9d6f61387f3141c07477f97425603"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b",
- "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b",
+ "url": "https://api.github.com/repos/laravel/sentinel/zipball/7a98db53e0d9d6f61387f3141c07477f97425603",
+ "reference": "7a98db53e0d9d6f61387f3141c07477f97425603",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.27",
+ "orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0",
+ "phpstan/phpstan": "^2.1.33"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Sentinel\\SentinelServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Sentinel\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Mior Muhammad Zaki",
+ "email": "mior@laravel.com"
+ }
+ ],
+ "support": {
+ "source": "https://github.com/laravel/sentinel/tree/v1.0.1"
+ },
+ "time": "2026-02-12T13:32:54+00:00"
+ },
+ {
+ "name": "laravel/serializable-closure",
+ "version": "v2.0.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/serializable-closure.git",
+ "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669",
+ "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
- "illuminate/support": "^10.0|^11.0|^12.0",
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"nesbot/carbon": "^2.67|^3.0",
"pestphp/pest": "^2.36|^3.0|^4.0",
"phpstan/phpstan": "^2.0",
- "symfony/var-dumper": "^6.2.0|^7.0.0"
+ "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0"
},
"type": "library",
"extra": {
@@ -2324,36 +2385,36 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-01-08T16:22:46+00:00"
+ "time": "2026-02-20T19:59:49+00:00"
},
{
"name": "laravel/socialite",
- "version": "v5.24.2",
+ "version": "v5.24.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613"
+ "reference": "0feb62267e7b8abc68593ca37639ad302728c129"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
- "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/0feb62267e7b8abc68593ca37639ad302728c129",
+ "reference": "0feb62267e7b8abc68593ca37639ad302728c129",
"shasum": ""
},
"require": {
"ext-json": "*",
"firebase/php-jwt": "^6.4|^7.0",
"guzzlehttp/guzzle": "^6.0|^7.0",
- "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"league/oauth1-client": "^1.11",
"php": "^7.2|^8.0",
"phpseclib/phpseclib": "^3.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
- "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8",
+ "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.12.23",
"phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0"
},
@@ -2396,20 +2457,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2026-01-10T16:07:28+00:00"
+ "time": "2026-02-21T13:32:50+00:00"
},
{
"name": "laravel/tinker",
- "version": "v2.11.0",
+ "version": "v2.11.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468"
+ "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741",
+ "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741",
"shasum": ""
},
"require": {
@@ -2460,9 +2521,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.11.0"
+ "source": "https://github.com/laravel/tinker/tree/v2.11.1"
},
- "time": "2025-12-19T19:16:45+00:00"
+ "time": "2026-02-06T14:12:35+00:00"
},
{
"name": "laravel/ui",
@@ -2791,16 +2852,16 @@
},
{
"name": "league/flysystem",
- "version": "3.31.0",
+ "version": "3.32.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff"
+ "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
- "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725",
+ "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725",
"shasum": ""
},
"require": {
@@ -2868,22 +2929,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.31.0"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.32.0"
},
- "time": "2026-01-23T15:38:47+00:00"
+ "time": "2026-02-25T17:01:41+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "3.31.0",
+ "version": "3.32.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "e36a2bc60b06332c92e4435047797ded352b446f"
+ "reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/e36a2bc60b06332c92e4435047797ded352b446f",
- "reference": "e36a2bc60b06332c92e4435047797ded352b446f",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/a1979df7c9784d334ea6df356aed3d18ac6673d0",
+ "reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0",
"shasum": ""
},
"require": {
@@ -2923,9 +2984,9 @@
"storage"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.31.0"
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.32.0"
},
- "time": "2026-01-23T15:30:45+00:00"
+ "time": "2026-02-25T16:46:44+00:00"
},
{
"name": "league/flysystem-local",
@@ -3341,36 +3402,36 @@
},
{
"name": "livewire/livewire",
- "version": "v3.7.8",
+ "version": "v3.7.11",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
- "reference": "06ec7e8cd61bb01739b8f26396db6fe73b7e0607"
+ "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/livewire/zipball/06ec7e8cd61bb01739b8f26396db6fe73b7e0607",
- "reference": "06ec7e8cd61bb01739b8f26396db6fe73b7e0607",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/addd6e8e9234df75f29e6a327ee2a745a7d67bb6",
+ "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6",
"shasum": ""
},
"require": {
- "illuminate/database": "^10.0|^11.0|^12.0",
- "illuminate/routing": "^10.0|^11.0|^12.0",
- "illuminate/support": "^10.0|^11.0|^12.0",
- "illuminate/validation": "^10.0|^11.0|^12.0",
+ "illuminate/database": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/routing": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/validation": "^10.0|^11.0|^12.0|^13.0",
"laravel/prompts": "^0.1.24|^0.2|^0.3",
"league/mime-type-detection": "^1.9",
"php": "^8.1",
- "symfony/console": "^6.0|^7.0",
- "symfony/http-kernel": "^6.2|^7.0"
+ "symfony/console": "^6.0|^7.0|^8.0",
+ "symfony/http-kernel": "^6.2|^7.0|^8.0"
},
"require-dev": {
"calebporzio/sushi": "^2.1",
- "laravel/framework": "^10.15.0|^11.0|^12.0",
+ "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0",
"mockery/mockery": "^1.3.1",
- "orchestra/testbench": "^8.21.0|^9.0|^10.0",
- "orchestra/testbench-dusk": "^8.24|^9.1|^10.0",
- "phpunit/phpunit": "^10.4|^11.5",
+ "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0",
+ "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0",
+ "phpunit/phpunit": "^10.4|^11.5|^12.5",
"psy/psysh": "^0.11.22|^0.12"
},
"type": "library",
@@ -3405,7 +3466,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
- "source": "https://github.com/livewire/livewire/tree/v3.7.8"
+ "source": "https://github.com/livewire/livewire/tree/v3.7.11"
},
"funding": [
{
@@ -3413,7 +3474,7 @@
"type": "github"
}
],
- "time": "2026-02-03T02:57:56+00:00"
+ "time": "2026-02-26T00:58:19+00:00"
},
{
"name": "log1x/laravel-webfonts",
@@ -3901,16 +3962,16 @@
},
{
"name": "nette/schema",
- "version": "v1.3.3",
+ "version": "v1.3.5",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
- "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
+ "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
- "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
+ "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
+ "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
"shasum": ""
},
"require": {
@@ -3918,8 +3979,10 @@
"php": "8.1 - 8.5"
},
"require-dev": {
- "nette/tester": "^2.5.2",
- "phpstan/phpstan-nette": "^2.0@stable",
+ "nette/phpstan-rules": "^1.0",
+ "nette/tester": "^2.6",
+ "phpstan/extension-installer": "^1.4@stable",
+ "phpstan/phpstan": "^2.1.39@stable",
"tracy/tracy": "^2.8"
},
"type": "library",
@@ -3960,22 +4023,22 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
- "source": "https://github.com/nette/schema/tree/v1.3.3"
+ "source": "https://github.com/nette/schema/tree/v1.3.5"
},
- "time": "2025-10-30T22:57:59+00:00"
+ "time": "2026-02-23T03:47:12+00:00"
},
{
"name": "nette/utils",
- "version": "v4.1.2",
+ "version": "v4.1.3",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5"
+ "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
- "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
+ "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe",
+ "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe",
"shasum": ""
},
"require": {
@@ -3987,8 +4050,10 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
+ "nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
- "phpstan/phpstan": "^2.0@stable",
+ "phpstan/extension-installer": "^1.4@stable",
+ "phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -4049,9 +4114,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.1.2"
+ "source": "https://github.com/nette/utils/tree/v4.1.3"
},
- "time": "2026-02-03T17:21:09+00:00"
+ "time": "2026-02-13T03:05:33+00:00"
},
{
"name": "nikic/php-parser",
@@ -4166,31 +4231,31 @@
},
{
"name": "nunomaduro/termwind",
- "version": "v2.3.3",
+ "version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
- "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017"
+ "reference": "712a31b768f5daea284c2169a7d227031001b9a8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017",
- "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017",
+ "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8",
+ "reference": "712a31b768f5daea284c2169a7d227031001b9a8",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.2",
- "symfony/console": "^7.3.6"
+ "symfony/console": "^7.4.4 || ^8.0.4"
},
"require-dev": {
- "illuminate/console": "^11.46.1",
- "laravel/pint": "^1.25.1",
+ "illuminate/console": "^11.47.0",
+ "laravel/pint": "^1.27.1",
"mockery/mockery": "^1.6.12",
- "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3",
+ "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2",
"phpstan/phpstan": "^1.12.32",
"phpstan/phpstan-strict-rules": "^1.6.2",
- "symfony/var-dumper": "^7.3.5",
+ "symfony/var-dumper": "^7.3.5 || ^8.0.4",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
@@ -4222,7 +4287,7 @@
"email": "enunomaduro@gmail.com"
}
],
- "description": "Its like Tailwind CSS, but for the console.",
+ "description": "It's like Tailwind CSS, but for the console.",
"keywords": [
"cli",
"console",
@@ -4233,7 +4298,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
- "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3"
+ "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0"
},
"funding": [
{
@@ -4249,7 +4314,7 @@
"type": "github"
}
],
- "time": "2025-11-20T02:34:59+00:00"
+ "time": "2026-02-16T23:10:27+00:00"
},
{
"name": "nyholm/psr7",
@@ -5776,16 +5841,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.19",
+ "version": "v0.12.20",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee"
+ "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/a4f766e5c5b6773d8399711019bb7d90875a50ee",
- "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373",
+ "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373",
"shasum": ""
},
"require": {
@@ -5849,9 +5914,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.19"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.20"
},
- "time": "2026-01-30T17:33:13+00:00"
+ "time": "2026-02-11T15:05:28+00:00"
},
{
"name": "purplepixie/phpdns",
@@ -6288,16 +6353,16 @@
},
{
"name": "sentry/sentry",
- "version": "4.19.1",
+ "version": "4.21.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
- "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3"
+ "reference": "2bf405fc4d38f00073a7d023cf321e59f614d54c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/1c21d60bebe67c0122335bd3fe977990435af0a3",
- "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3",
+ "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/2bf405fc4d38f00073a7d023cf321e59f614d54c",
+ "reference": "2bf405fc4d38f00073a7d023cf321e59f614d54c",
"shasum": ""
},
"require": {
@@ -6318,9 +6383,12 @@
"guzzlehttp/promises": "^2.0.3",
"guzzlehttp/psr7": "^1.8.4|^2.1.1",
"monolog/monolog": "^1.6|^2.0|^3.0",
+ "nyholm/psr7": "^1.8",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "^1.3",
- "phpunit/phpunit": "^8.5|^9.6",
+ "phpunit/phpunit": "^8.5.52|^9.6.34",
+ "spiral/roadrunner-http": "^3.6",
+ "spiral/roadrunner-worker": "^3.6",
"vimeo/psalm": "^4.17"
},
"suggest": {
@@ -6360,7 +6428,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
- "source": "https://github.com/getsentry/sentry-php/tree/4.19.1"
+ "source": "https://github.com/getsentry/sentry-php/tree/4.21.0"
},
"funding": [
{
@@ -6372,27 +6440,27 @@
"type": "custom"
}
],
- "time": "2025-12-02T15:57:41+00:00"
+ "time": "2026-02-24T15:32:51+00:00"
},
{
"name": "sentry/sentry-laravel",
- "version": "4.20.1",
+ "version": "4.21.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
- "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72"
+ "reference": "4b939116c2d3c5de328f23a5f1dfb97b40e0c17b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/503853fa7ee74b34b64e76f1373db86cd11afe72",
- "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72",
+ "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/4b939116c2d3c5de328f23a5f1dfb97b40e0c17b",
+ "reference": "4b939116c2d3c5de328f23a5f1dfb97b40e0c17b",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
"nyholm/psr7": "^1.0",
"php": "^7.2 | ^8.0",
- "sentry/sentry": "^4.19.0",
+ "sentry/sentry": "^4.21.0",
"symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0"
},
"require-dev": {
@@ -6405,7 +6473,7 @@
"mockery/mockery": "^1.3",
"orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4 | ^11.5"
+ "phpunit/phpunit": "^8.5 | ^9.6 | ^10.4 | ^11.5"
},
"type": "library",
"extra": {
@@ -6450,7 +6518,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-laravel/issues",
- "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.1"
+ "source": "https://github.com/getsentry/sentry-laravel/tree/4.21.0"
},
"funding": [
{
@@ -6462,20 +6530,20 @@
"type": "custom"
}
],
- "time": "2026-01-07T08:53:19+00:00"
+ "time": "2026-02-26T16:08:52+00:00"
},
{
"name": "socialiteproviders/authentik",
- "version": "5.2.0",
+ "version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Authentik.git",
- "reference": "4cf129cf04728a38e0531c54454464b162f0fa66"
+ "reference": "4ef0ca226d3be29dc0523f3afc86b63fd6b755b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4cf129cf04728a38e0531c54454464b162f0fa66",
- "reference": "4cf129cf04728a38e0531c54454464b162f0fa66",
+ "url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4ef0ca226d3be29dc0523f3afc86b63fd6b755b4",
+ "reference": "4ef0ca226d3be29dc0523f3afc86b63fd6b755b4",
"shasum": ""
},
"require": {
@@ -6512,7 +6580,7 @@
"issues": "https://github.com/socialiteproviders/providers/issues",
"source": "https://github.com/socialiteproviders/providers"
},
- "time": "2023-11-07T22:21:16+00:00"
+ "time": "2026-02-04T14:27:03+00:00"
},
{
"name": "socialiteproviders/clerk",
@@ -7007,29 +7075,29 @@
},
{
"name": "spatie/laravel-activitylog",
- "version": "4.11.0",
+ "version": "4.12.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-activitylog.git",
- "reference": "cd7c458f0e128e56eb2d71977d67a846ce4cc10f"
+ "reference": "bf66b5bbe9a946e977e876420d16b30b9aff1b2d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/cd7c458f0e128e56eb2d71977d67a846ce4cc10f",
- "reference": "cd7c458f0e128e56eb2d71977d67a846ce4cc10f",
+ "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bf66b5bbe9a946e977e876420d16b30b9aff1b2d",
+ "reference": "bf66b5bbe9a946e977e876420d16b30b9aff1b2d",
"shasum": ""
},
"require": {
- "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
- "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0",
- "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
+ "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
+ "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
+ "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.6.3"
},
"require-dev": {
"ext-json": "*",
- "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0",
- "pestphp/pest": "^1.20 || ^2.0 || ^3.0"
+ "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0",
+ "pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0"
},
"type": "library",
"extra": {
@@ -7082,7 +7150,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-activitylog/issues",
- "source": "https://github.com/spatie/laravel-activitylog/tree/4.11.0"
+ "source": "https://github.com/spatie/laravel-activitylog/tree/4.12.1"
},
"funding": [
{
@@ -7094,24 +7162,24 @@
"type": "github"
}
],
- "time": "2026-01-31T12:25:02+00:00"
+ "time": "2026-02-22T08:37:18+00:00"
},
{
"name": "spatie/laravel-data",
- "version": "4.19.1",
+ "version": "4.20.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-data.git",
- "reference": "41ed0472250676f19440fb24d7b62a8d43abdb89"
+ "reference": "05b792ab0e059d26eca15d47d199ba6f4c96054e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-data/zipball/41ed0472250676f19440fb24d7b62a8d43abdb89",
- "reference": "41ed0472250676f19440fb24d7b62a8d43abdb89",
+ "url": "https://api.github.com/repos/spatie/laravel-data/zipball/05b792ab0e059d26eca15d47d199ba6f4c96054e",
+ "reference": "05b792ab0e059d26eca15d47d199ba6f4c96054e",
"shasum": ""
},
"require": {
- "illuminate/contracts": "^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
"phpdocumentor/reflection": "^6.0",
"spatie/laravel-package-tools": "^1.9.0",
@@ -7121,10 +7189,10 @@
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
"inertiajs/inertia-laravel": "^2.0",
- "livewire/livewire": "^3.0",
+ "livewire/livewire": "^3.0|^4.0",
"mockery/mockery": "^1.6",
"nesbot/carbon": "^2.63|^3.0",
- "orchestra/testbench": "^8.37.0|^9.16|^10.9",
+ "orchestra/testbench": "^8.37.0|^9.16|^10.9|^11.0",
"pestphp/pest": "^2.36|^3.8|^4.3",
"pestphp/pest-plugin-laravel": "^2.4|^3.0|^4.0",
"pestphp/pest-plugin-livewire": "^2.1|^3.0|^4.0",
@@ -7168,7 +7236,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-data/issues",
- "source": "https://github.com/spatie/laravel-data/tree/4.19.1"
+ "source": "https://github.com/spatie/laravel-data/tree/4.20.0"
},
"funding": [
{
@@ -7176,27 +7244,27 @@
"type": "github"
}
],
- "time": "2026-01-28T13:10:20+00:00"
+ "time": "2026-02-25T16:18:18+00:00"
},
{
"name": "spatie/laravel-markdown",
- "version": "2.7.1",
+ "version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-markdown.git",
- "reference": "353e7f9fae62826e26cbadef58a12ecf39685280"
+ "reference": "eabe8c7e31c2739ad0fe63ba04eb2e3189608187"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/353e7f9fae62826e26cbadef58a12ecf39685280",
- "reference": "353e7f9fae62826e26cbadef58a12ecf39685280",
+ "url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/eabe8c7e31c2739ad0fe63ba04eb2e3189608187",
+ "reference": "eabe8c7e31c2739ad0fe63ba04eb2e3189608187",
"shasum": ""
},
"require": {
- "illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
- "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
- "illuminate/support": "^9.0|^10.0|^11.0|^12.0",
- "illuminate/view": "^9.0|^10.0|^11.0|^12.0",
+ "illuminate/cache": "^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/view": "^9.0|^10.0|^11.0|^12.0|^13.0",
"league/commonmark": "^2.6.0",
"php": "^8.1",
"spatie/commonmark-shiki-highlighter": "^2.5",
@@ -7205,9 +7273,9 @@
"require-dev": {
"brianium/paratest": "^6.2|^7.8",
"nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
- "orchestra/testbench": "^6.15|^7.0|^8.0|^10.0",
- "pestphp/pest": "^1.22|^2.0|^3.7",
- "phpunit/phpunit": "^9.3|^11.5.3",
+ "orchestra/testbench": "^6.15|^7.0|^8.0|^10.0|^11.0",
+ "pestphp/pest": "^1.22|^2.0|^3.7|^4.4",
+ "phpunit/phpunit": "^9.3|^11.5.3|^12.5.12",
"spatie/laravel-ray": "^1.23",
"spatie/pest-plugin-snapshots": "^1.1|^2.2|^3.0",
"vimeo/psalm": "^4.8|^6.7"
@@ -7244,7 +7312,7 @@
"spatie"
],
"support": {
- "source": "https://github.com/spatie/laravel-markdown/tree/2.7.1"
+ "source": "https://github.com/spatie/laravel-markdown/tree/2.8.0"
},
"funding": [
{
@@ -7252,33 +7320,33 @@
"type": "github"
}
],
- "time": "2025-02-21T13:43:18+00:00"
+ "time": "2026-02-22T18:53:36+00:00"
},
{
"name": "spatie/laravel-package-tools",
- "version": "1.92.7",
+ "version": "1.93.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
- "reference": "f09a799850b1ed765103a4f0b4355006360c49a5"
+ "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5",
- "reference": "f09a799850b1ed765103a4f0b4355006360c49a5",
+ "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
+ "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
"shasum": ""
},
"require": {
- "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
- "php": "^8.0"
+ "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
+ "php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.5",
- "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
- "pestphp/pest": "^1.23|^2.1|^3.1",
- "phpunit/php-code-coverage": "^9.0|^10.0|^11.0",
- "phpunit/phpunit": "^9.5.24|^10.5|^11.5",
- "spatie/pest-plugin-test-time": "^1.1|^2.2"
+ "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
+ "pestphp/pest": "^2.1|^3.1|^4.0",
+ "phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
+ "phpunit/phpunit": "^10.5|^11.5|^12.5",
+ "spatie/pest-plugin-test-time": "^2.2|^3.0"
},
"type": "library",
"autoload": {
@@ -7305,7 +7373,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
- "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7"
+ "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0"
},
"funding": [
{
@@ -7313,29 +7381,29 @@
"type": "github"
}
],
- "time": "2025-07-17T15:46:43+00:00"
+ "time": "2026-02-21T12:49:54+00:00"
},
{
"name": "spatie/laravel-ray",
- "version": "1.43.5",
+ "version": "1.43.6",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ray.git",
- "reference": "2003e627d4a17e8411fff18153e47a754f0c028d"
+ "reference": "117a4addce2cb8adfc01b864435b5b278e2f0c40"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/2003e627d4a17e8411fff18153e47a754f0c028d",
- "reference": "2003e627d4a17e8411fff18153e47a754f0c028d",
+ "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/117a4addce2cb8adfc01b864435b5b278e2f0c40",
+ "reference": "117a4addce2cb8adfc01b864435b5b278e2f0c40",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2.2",
"ext-json": "*",
- "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
- "illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
- "illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
- "illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^7.4|^8.0",
"spatie/backtrace": "^1.7.1",
"spatie/ray": "^1.45.0",
@@ -7344,9 +7412,9 @@
},
"require-dev": {
"guzzlehttp/guzzle": "^7.3",
- "laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
+ "laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
"laravel/pint": "^1.27",
- "orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
+ "orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"pestphp/pest": "^1.22|^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.10.57|^2.0.2",
"phpunit/phpunit": "^9.3|^10.1|^11.0.10|^12.4",
@@ -7390,7 +7458,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-ray/issues",
- "source": "https://github.com/spatie/laravel-ray/tree/1.43.5"
+ "source": "https://github.com/spatie/laravel-ray/tree/1.43.6"
},
"funding": [
{
@@ -7402,26 +7470,26 @@
"type": "other"
}
],
- "time": "2026-01-26T19:05:19+00:00"
+ "time": "2026-02-19T10:24:51+00:00"
},
{
"name": "spatie/laravel-schemaless-attributes",
- "version": "2.5.1",
+ "version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-schemaless-attributes.git",
- "reference": "3561875fb6886ae55e5378f20ba5ac87f20b265a"
+ "reference": "7d17ab5f434ae47324b849e007ce80669966c14e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-schemaless-attributes/zipball/3561875fb6886ae55e5378f20ba5ac87f20b265a",
- "reference": "3561875fb6886ae55e5378f20ba5ac87f20b265a",
+ "url": "https://api.github.com/repos/spatie/laravel-schemaless-attributes/zipball/7d17ab5f434ae47324b849e007ce80669966c14e",
+ "reference": "7d17ab5f434ae47324b849e007ce80669966c14e",
"shasum": ""
},
"require": {
- "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "illuminate/database": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/database": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^8.0",
"spatie/laravel-package-tools": "^1.4.3"
},
@@ -7429,9 +7497,9 @@
"brianium/paratest": "^6.2|^7.4",
"mockery/mockery": "^1.4",
"nunomaduro/collision": "^5.3|^6.0|^8.0",
- "orchestra/testbench": "^6.15|^7.0|^8.0|^9.0|^10.0",
- "pestphp/pest-plugin-laravel": "^1.3|^2.1|^3.1",
- "phpunit/phpunit": "^9.6|^10.5|^11.5|^12.0"
+ "orchestra/testbench": "^6.15|^7.0|^8.0|^9.0|^10.0|^11.0",
+ "pestphp/pest-plugin-laravel": "^1.3|^2.1|^3.1|^4.0",
+ "phpunit/phpunit": "^9.6|^10.5|^11.5|^12.3"
},
"type": "library",
"extra": {
@@ -7466,7 +7534,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-schemaless-attributes/issues",
- "source": "https://github.com/spatie/laravel-schemaless-attributes/tree/2.5.1"
+ "source": "https://github.com/spatie/laravel-schemaless-attributes/tree/2.6.0"
},
"funding": [
{
@@ -7478,7 +7546,7 @@
"type": "github"
}
],
- "time": "2025-02-10T09:28:22+00:00"
+ "time": "2026-02-21T15:13:56+00:00"
},
{
"name": "spatie/macroable",
@@ -7533,29 +7601,29 @@
},
{
"name": "spatie/php-structure-discoverer",
- "version": "2.3.3",
+ "version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/php-structure-discoverer.git",
- "reference": "552a5b974a9853a32e5677a66e85ae615a96a90b"
+ "reference": "9a53c79b48fca8b6d15faa8cbba47cc430355146"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/552a5b974a9853a32e5677a66e85ae615a96a90b",
- "reference": "552a5b974a9853a32e5677a66e85ae615a96a90b",
+ "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/9a53c79b48fca8b6d15faa8cbba47cc430355146",
+ "reference": "9a53c79b48fca8b6d15faa8cbba47cc430355146",
"shasum": ""
},
"require": {
- "illuminate/collections": "^11.0|^12.0",
+ "illuminate/collections": "^11.0|^12.0|^13.0",
"php": "^8.3",
"spatie/laravel-package-tools": "^1.92.7",
"symfony/finder": "^6.0|^7.3.5|^8.0"
},
"require-dev": {
"amphp/parallel": "^2.3.2",
- "illuminate/console": "^11.0|^12.0",
+ "illuminate/console": "^11.0|^12.0|^13.0",
"nunomaduro/collision": "^7.0|^8.8.3",
- "orchestra/testbench": "^9.5|^10.8",
+ "orchestra/testbench": "^9.5|^10.8|^11.0",
"pestphp/pest": "^3.8|^4.0",
"pestphp/pest-plugin-laravel": "^3.2|^4.0",
"phpstan/extension-installer": "^1.4.3",
@@ -7600,7 +7668,7 @@
],
"support": {
"issues": "https://github.com/spatie/php-structure-discoverer/issues",
- "source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.3"
+ "source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.0"
},
"funding": [
{
@@ -7608,20 +7676,20 @@
"type": "github"
}
],
- "time": "2025-11-24T16:41:01+00:00"
+ "time": "2026-02-21T15:57:15+00:00"
},
{
"name": "spatie/ray",
- "version": "1.45.0",
+ "version": "1.47.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ray.git",
- "reference": "68920c418d10fe103722d366faa575533d26434f"
+ "reference": "3112acb6a7fbcefe35f6e47b1dc13341ff5bc5ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/ray/zipball/68920c418d10fe103722d366faa575533d26434f",
- "reference": "68920c418d10fe103722d366faa575533d26434f",
+ "url": "https://api.github.com/repos/spatie/ray/zipball/3112acb6a7fbcefe35f6e47b1dc13341ff5bc5ce",
+ "reference": "3112acb6a7fbcefe35f6e47b1dc13341ff5bc5ce",
"shasum": ""
},
"require": {
@@ -7635,7 +7703,7 @@
"symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3|^8.0"
},
"require-dev": {
- "illuminate/support": "^7.20|^8.18|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/support": "^7.20|^8.18|^9.0|^10.0|^11.0|^12.0|^13.0",
"nesbot/carbon": "^2.63|^3.8.4",
"pestphp/pest": "^1.22",
"phpstan/phpstan": "^1.10.57|^2.0.3",
@@ -7681,7 +7749,7 @@
],
"support": {
"issues": "https://github.com/spatie/ray/issues",
- "source": "https://github.com/spatie/ray/tree/1.45.0"
+ "source": "https://github.com/spatie/ray/tree/1.47.0"
},
"funding": [
{
@@ -7693,7 +7761,7 @@
"type": "other"
}
],
- "time": "2026-01-26T18:45:30+00:00"
+ "time": "2026-02-20T20:42:26+00:00"
},
{
"name": "spatie/shiki-php",
@@ -8026,16 +8094,16 @@
},
{
"name": "symfony/console",
- "version": "v7.4.4",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894"
+ "reference": "6d643a93b47398599124022eb24d97c153c12f27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
- "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
+ "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27",
+ "reference": "6d643a93b47398599124022eb24d97c153c12f27",
"shasum": ""
},
"require": {
@@ -8100,7 +8168,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.4"
+ "source": "https://github.com/symfony/console/tree/v7.4.6"
},
"funding": [
{
@@ -8120,20 +8188,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-13T11:36:38+00:00"
+ "time": "2026-02-25T17:02:47+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v8.0.0",
+ "version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b"
+ "reference": "2a178bf80f05dbbe469a337730eba79d61315262"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b",
- "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262",
+ "reference": "2a178bf80f05dbbe469a337730eba79d61315262",
"shasum": ""
},
"require": {
@@ -8169,7 +8237,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v8.0.0"
+ "source": "https://github.com/symfony/css-selector/tree/v8.0.6"
},
"funding": [
{
@@ -8189,7 +8257,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T14:17:19+00:00"
+ "time": "2026-02-17T13:07:04+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -8503,16 +8571,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v8.0.1",
+ "version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d"
+ "reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
+ "reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
"shasum": ""
},
"require": {
@@ -8549,7 +8617,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v8.0.1"
+ "source": "https://github.com/symfony/filesystem/tree/v8.0.6"
},
"funding": [
{
@@ -8569,20 +8637,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-01T09:13:36+00:00"
+ "time": "2026-02-25T16:59:43+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.4.5",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
+ "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
- "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
+ "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
"shasum": ""
},
"require": {
@@ -8617,7 +8685,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.4.5"
+ "source": "https://github.com/symfony/finder/tree/v7.4.6"
},
"funding": [
{
@@ -8637,20 +8705,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-26T15:07:59+00:00"
+ "time": "2026-01-29T09:40:50+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v7.4.5",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "446d0db2b1f21575f1284b74533e425096abdfb6"
+ "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6",
- "reference": "446d0db2b1f21575f1284b74533e425096abdfb6",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065",
+ "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065",
"shasum": ""
},
"require": {
@@ -8699,7 +8767,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.4.5"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.6"
},
"funding": [
{
@@ -8719,20 +8787,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-27T16:16:02+00:00"
+ "time": "2026-02-21T16:25:55+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.4.5",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a"
+ "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a",
- "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83",
+ "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83",
"shasum": ""
},
"require": {
@@ -8774,7 +8842,7 @@
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/css-selector": "^6.4|^7.0|^8.0",
- "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0",
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
@@ -8818,7 +8886,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.4.5"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.6"
},
"funding": [
{
@@ -8838,20 +8906,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-28T10:33:42+00:00"
+ "time": "2026-02-26T08:30:57+00:00"
},
{
"name": "symfony/mailer",
- "version": "v7.4.4",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6"
+ "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/7b750074c40c694ceb34cb926d6dffee231c5cd6",
- "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
+ "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
"shasum": ""
},
"require": {
@@ -8902,7 +8970,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v7.4.4"
+ "source": "https://github.com/symfony/mailer/tree/v7.4.6"
},
"funding": [
{
@@ -8922,20 +8990,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-08T08:25:11+00:00"
+ "time": "2026-02-25T16:50:00+00:00"
},
{
"name": "symfony/mime",
- "version": "v7.4.5",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148"
+ "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148",
- "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f",
+ "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f",
"shasum": ""
},
"require": {
@@ -8946,7 +9014,7 @@
},
"conflict": {
"egulias/email-validator": "~3.0.0",
- "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/reflection-docblock": "<5.2|>=7",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
@@ -8954,7 +9022,7 @@
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
- "phpdocumentor/reflection-docblock": "^5.2",
+ "phpdocumentor/reflection-docblock": "^5.2|^6.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
@@ -8991,7 +9059,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.4.5"
+ "source": "https://github.com/symfony/mime/tree/v7.4.6"
},
"funding": [
{
@@ -9011,7 +9079,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-27T08:59:58+00:00"
+ "time": "2026-02-05T15:57:06+00:00"
},
{
"name": "symfony/options-resolver",
@@ -10151,16 +10219,16 @@
},
{
"name": "symfony/routing",
- "version": "v7.4.4",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "0798827fe2c79caeed41d70b680c2c3507d10147"
+ "reference": "238d749c56b804b31a9bf3e26519d93b65a60938"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147",
- "reference": "0798827fe2c79caeed41d70b680c2c3507d10147",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938",
+ "reference": "238d749c56b804b31a9bf3e26519d93b65a60938",
"shasum": ""
},
"require": {
@@ -10212,7 +10280,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.4.4"
+ "source": "https://github.com/symfony/routing/tree/v7.4.6"
},
"funding": [
{
@@ -10232,7 +10300,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-12T12:19:02+00:00"
+ "time": "2026-02-25T16:50:00+00:00"
},
{
"name": "symfony/service-contracts",
@@ -10389,16 +10457,16 @@
},
{
"name": "symfony/string",
- "version": "v8.0.4",
+ "version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "758b372d6882506821ed666032e43020c4f57194"
+ "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
- "reference": "758b372d6882506821ed666032e43020c4f57194",
+ "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
+ "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -10455,7 +10523,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.4"
+ "source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -10475,20 +10543,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-12T12:37:40+00:00"
+ "time": "2026-02-09T10:14:57+00:00"
},
{
"name": "symfony/translation",
- "version": "v8.0.4",
+ "version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10"
+ "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/db70c8ce7db74fd2da7b1d268db46b2a8ce32c10",
- "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
+ "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
"shasum": ""
},
"require": {
@@ -10548,7 +10616,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v8.0.4"
+ "source": "https://github.com/symfony/translation/tree/v8.0.6"
},
"funding": [
{
@@ -10568,7 +10636,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-13T13:06:50+00:00"
+ "time": "2026-02-17T13:07:04+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -10732,16 +10800,16 @@
},
{
"name": "symfony/var-dumper",
- "version": "v7.4.4",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "0e4769b46a0c3c62390d124635ce59f66874b282"
+ "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282",
- "reference": "0e4769b46a0c3c62390d124635ce59f66874b282",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291",
+ "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291",
"shasum": ""
},
"require": {
@@ -10795,7 +10863,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.4.4"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.6"
},
"funding": [
{
@@ -10815,20 +10883,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-01T22:13:48+00:00"
+ "time": "2026-02-15T10:53:20+00:00"
},
{
"name": "symfony/yaml",
- "version": "v7.4.1",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "24dd4de28d2e3988b311751ac49e684d783e2345"
+ "reference": "58751048de17bae71c5aa0d13cb19d79bca26391"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345",
- "reference": "24dd4de28d2e3988b311751ac49e684d783e2345",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391",
+ "reference": "58751048de17bae71c5aa0d13cb19d79bca26391",
"shasum": ""
},
"require": {
@@ -10871,7 +10939,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.4.1"
+ "source": "https://github.com/symfony/yaml/tree/v7.4.6"
},
"funding": [
{
@@ -10891,7 +10959,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T18:11:45+00:00"
+ "time": "2026-02-09T09:33:46+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -10950,33 +11018,45 @@
},
{
"name": "visus/cuid2",
- "version": "4.1.0",
+ "version": "6.0.0",
"source": {
"type": "git",
"url": "https://github.com/visus-io/php-cuid2.git",
- "reference": "17c9b3098d556bb2556a084c948211333cc19c79"
+ "reference": "834c8a1c04684931600ee7a4189150b331a5b56c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/17c9b3098d556bb2556a084c948211333cc19c79",
- "reference": "17c9b3098d556bb2556a084c948211333cc19c79",
+ "url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/834c8a1c04684931600ee7a4189150b331a5b56c",
+ "reference": "834c8a1c04684931600ee7a4189150b331a5b56c",
"shasum": ""
},
"require": {
- "php": "^8.1"
+ "php": "^8.2",
+ "symfony/polyfill-php83": "^1.32"
},
"require-dev": {
+ "captainhook/captainhook": "^5.27",
+ "captainhook/hook-installer": "^1.0",
+ "captainhook/plugin-composer": "^5.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.2",
"ergebnis/composer-normalize": "^2.29",
- "ext-ctype": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpbench/phpbench": "^1.4",
"phpstan/phpstan": "^1.9",
- "phpunit/phpunit": "^10.0",
- "squizlabs/php_codesniffer": "^3.7",
- "vimeo/psalm": "^5.4"
+ "phpunit/phpunit": "^10.5",
+ "ramsey/conventional-commits": "^1.5",
+ "slevomat/coding-standard": "^8.25",
+ "squizlabs/php_codesniffer": "^4.0"
},
"suggest": {
- "ext-gmp": "*"
+ "ext-gmp": "Enables faster math with arbitrary precision integers using GMP."
},
"type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ }
+ },
"autoload": {
"files": [
"src/compat.php"
@@ -11002,9 +11082,9 @@
],
"support": {
"issues": "https://github.com/visus-io/php-cuid2/issues",
- "source": "https://github.com/visus-io/php-cuid2/tree/4.1.0"
+ "source": "https://github.com/visus-io/php-cuid2/tree/6.0.0"
},
- "time": "2024-05-14T13:23:35+00:00"
+ "time": "2025-12-18T14:52:27+00:00"
},
{
"name": "vlucas/phpdotenv",
@@ -11542,16 +11622,16 @@
},
{
"name": "zircote/swagger-php",
- "version": "5.8.0",
+ "version": "5.8.3",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
- "reference": "9cf5d1a0c159894026708c9e837e69140c2d3922"
+ "reference": "098223019f764a16715f64089a58606096719c98"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zircote/swagger-php/zipball/9cf5d1a0c159894026708c9e837e69140c2d3922",
- "reference": "9cf5d1a0c159894026708c9e837e69140c2d3922",
+ "url": "https://api.github.com/repos/zircote/swagger-php/zipball/098223019f764a16715f64089a58606096719c98",
+ "reference": "098223019f764a16715f64089a58606096719c98",
"shasum": ""
},
"require": {
@@ -11624,7 +11704,7 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
- "source": "https://github.com/zircote/swagger-php/tree/5.8.0"
+ "source": "https://github.com/zircote/swagger-php/tree/5.8.3"
},
"funding": [
{
@@ -11632,7 +11712,7 @@
"type": "github"
}
],
- "time": "2026-01-28T01:27:48+00:00"
+ "time": "2026-03-02T00:47:18+00:00"
}
],
"packages-dev": [
@@ -12945,16 +13025,16 @@
},
{
"name": "brianium/paratest",
- "version": "v7.16.1",
+ "version": "v7.19.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b"
+ "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
+ "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"shasum": ""
},
"require": {
@@ -12965,24 +13045,24 @@
"fidry/cpu-core-counter": "^1.3.0",
"jean85/pretty-package-versions": "^2.1.1",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
- "phpunit/php-code-coverage": "^12.5.2",
- "phpunit/php-file-iterator": "^6",
- "phpunit/php-timer": "^8",
- "phpunit/phpunit": "^12.5.4",
- "sebastian/environment": "^8.0.3",
- "symfony/console": "^7.3.4 || ^8.0.0",
- "symfony/process": "^7.3.4 || ^8.0.0"
+ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
+ "phpunit/php-file-iterator": "^6.0.1 || ^7",
+ "phpunit/php-timer": "^8 || ^9",
+ "phpunit/phpunit": "^12.5.9 || ^13",
+ "sebastian/environment": "^8.0.3 || ^9",
+ "symfony/console": "^7.4.4 || ^8.0.4",
+ "symfony/process": "^7.4.5 || ^8.0.5"
},
"require-dev": {
"doctrine/coding-standard": "^14.0.0",
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
- "phpstan/phpstan": "^2.1.33",
+ "phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.11",
- "phpstan/phpstan-strict-rules": "^2.0.7",
- "symfony/filesystem": "^7.3.2 || ^8.0.0"
+ "phpstan/phpstan-phpunit": "^2.0.12",
+ "phpstan/phpstan-strict-rules": "^2.0.8",
+ "symfony/filesystem": "^7.4.0 || ^8.0.1"
},
"bin": [
"bin/paratest",
@@ -13022,7 +13102,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.16.1"
+ "source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
},
"funding": [
{
@@ -13034,7 +13114,7 @@
"type": "paypal"
}
],
- "time": "2026-01-08T07:23:06+00:00"
+ "time": "2026-02-06T10:53:26+00:00"
},
{
"name": "daverandom/libdns",
@@ -13422,16 +13502,16 @@
},
{
"name": "laravel/boost",
- "version": "v2.1.1",
+ "version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "1c7d6f44c96937a961056778b9143218b1183302"
+ "reference": "e27f1616177377fef95296620530c44a7dda4df9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/1c7d6f44c96937a961056778b9143218b1183302",
- "reference": "1c7d6f44c96937a961056778b9143218b1183302",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/e27f1616177377fef95296620530c44a7dda4df9",
+ "reference": "e27f1616177377fef95296620530c44a7dda4df9",
"shasum": ""
},
"require": {
@@ -13442,7 +13522,7 @@
"illuminate/support": "^11.45.3|^12.41.1",
"laravel/mcp": "^0.5.1",
"laravel/prompts": "^0.3.10",
- "laravel/roster": "^0.2.9",
+ "laravel/roster": "^0.5.0",
"php": "^8.2"
},
"require-dev": {
@@ -13484,43 +13564,43 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2026-02-06T10:41:29+00:00"
+ "time": "2026-02-25T16:07:36+00:00"
},
{
"name": "laravel/dusk",
- "version": "v8.3.4",
+ "version": "v8.3.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
- "reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6"
+ "reference": "5c3beee54f91f575f50cadcd7e5d44c80cc9a9aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/dusk/zipball/33a4211c7b63ffe430bf30ec3c014012dcb6dfa6",
- "reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6",
+ "url": "https://api.github.com/repos/laravel/dusk/zipball/5c3beee54f91f575f50cadcd7e5d44c80cc9a9aa",
+ "reference": "5c3beee54f91f575f50cadcd7e5d44c80cc9a9aa",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"guzzlehttp/guzzle": "^7.5",
- "illuminate/console": "^10.0|^11.0|^12.0",
- "illuminate/support": "^10.0|^11.0|^12.0",
+ "illuminate/console": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
"php-webdriver/webdriver": "^1.15.2",
- "symfony/console": "^6.2|^7.0",
- "symfony/finder": "^6.2|^7.0",
- "symfony/process": "^6.2|^7.0",
+ "symfony/console": "^6.2|^7.0|^8.0",
+ "symfony/finder": "^6.2|^7.0|^8.0",
+ "symfony/process": "^6.2|^7.0|^8.0",
"vlucas/phpdotenv": "^5.2"
},
"require-dev": {
- "laravel/framework": "^10.0|^11.0|^12.0",
+ "laravel/framework": "^10.0|^11.0|^12.0|^13.0",
"mockery/mockery": "^1.6",
- "orchestra/testbench-core": "^8.19|^9.17|^10.8",
+ "orchestra/testbench-core": "^8.19|^9.17|^10.8|^11.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.1|^11.0|^12.0.1",
"psy/psysh": "^0.11.12|^0.12",
- "symfony/yaml": "^6.2|^7.0"
+ "symfony/yaml": "^6.2|^7.0|^8.0"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
@@ -13556,22 +13636,22 @@
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
- "source": "https://github.com/laravel/dusk/tree/v8.3.4"
+ "source": "https://github.com/laravel/dusk/tree/v8.3.6"
},
- "time": "2025-11-20T16:26:16+00:00"
+ "time": "2026-02-10T18:14:59+00:00"
},
{
"name": "laravel/mcp",
- "version": "v0.5.5",
+ "version": "v0.5.9",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
- "reference": "b3327bb75fd2327577281e507e2dbc51649513d6"
+ "reference": "39e8da60eb7bce4737c5d868d35a3fe78938c129"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/mcp/zipball/b3327bb75fd2327577281e507e2dbc51649513d6",
- "reference": "b3327bb75fd2327577281e507e2dbc51649513d6",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/39e8da60eb7bce4737c5d868d35a3fe78938c129",
+ "reference": "39e8da60eb7bce4737c5d868d35a3fe78938c129",
"shasum": ""
},
"require": {
@@ -13631,20 +13711,20 @@
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
- "time": "2026-02-05T14:05:18+00:00"
+ "time": "2026-02-17T19:05:53+00:00"
},
{
"name": "laravel/pint",
- "version": "v1.27.0",
+ "version": "v1.27.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
+ "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5",
+ "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5",
"shasum": ""
},
"require": {
@@ -13655,13 +13735,13 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.92.4",
- "illuminate/view": "^12.44.0",
- "larastan/larastan": "^3.8.1",
- "laravel-zero/framework": "^12.0.4",
+ "friendsofphp/php-cs-fixer": "^3.93.1",
+ "illuminate/view": "^12.51.0",
+ "larastan/larastan": "^3.9.2",
+ "laravel-zero/framework": "^12.0.5",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
- "pestphp/pest": "^3.8.4"
+ "pestphp/pest": "^3.8.5"
},
"bin": [
"builds/pint"
@@ -13698,35 +13778,35 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2026-01-05T16:49:17+00:00"
+ "time": "2026-02-10T20:00:20+00:00"
},
{
"name": "laravel/roster",
- "version": "v0.2.9",
+ "version": "v0.5.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/roster.git",
- "reference": "82bbd0e2de614906811aebdf16b4305956816fa6"
+ "reference": "56904a78f4d7360c1c490ced7deeebf9aecb8c0e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6",
- "reference": "82bbd0e2de614906811aebdf16b4305956816fa6",
+ "url": "https://api.github.com/repos/laravel/roster/zipball/56904a78f4d7360c1c490ced7deeebf9aecb8c0e",
+ "reference": "56904a78f4d7360c1c490ced7deeebf9aecb8c0e",
"shasum": ""
},
"require": {
- "illuminate/console": "^10.0|^11.0|^12.0",
- "illuminate/contracts": "^10.0|^11.0|^12.0",
- "illuminate/routing": "^10.0|^11.0|^12.0",
- "illuminate/support": "^10.0|^11.0|^12.0",
- "php": "^8.1|^8.2",
- "symfony/yaml": "^6.4|^7.2"
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "illuminate/routing": "^11.0|^12.0|^13.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
+ "php": "^8.2",
+ "symfony/yaml": "^7.2|^8.0"
},
"require-dev": {
"laravel/pint": "^1.14",
"mockery/mockery": "^1.6",
- "orchestra/testbench": "^8.22.0|^9.0|^10.0",
- "pestphp/pest": "^2.0|^3.0",
+ "orchestra/testbench": "^9.0|^10.0|^11.0",
+ "pestphp/pest": "^3.0|^4.1",
"phpstan/phpstan": "^2.0"
},
"type": "library",
@@ -13759,25 +13839,26 @@
"issues": "https://github.com/laravel/roster/issues",
"source": "https://github.com/laravel/roster"
},
- "time": "2025-10-20T09:56:46+00:00"
+ "time": "2026-02-17T17:33:35+00:00"
},
{
"name": "laravel/telescope",
- "version": "v5.16.1",
+ "version": "5.18.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
- "reference": "dc114b94f025b8c16b5eb3194b4ddc0e46d5310c"
+ "reference": "8bbc1d839317cef7106cabf028e407416e5a1dad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/telescope/zipball/dc114b94f025b8c16b5eb3194b4ddc0e46d5310c",
- "reference": "dc114b94f025b8c16b5eb3194b4ddc0e46d5310c",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/8bbc1d839317cef7106cabf028e407416e5a1dad",
+ "reference": "8bbc1d839317cef7106cabf028e407416e5a1dad",
"shasum": ""
},
"require": {
"ext-json": "*",
- "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "laravel/sentinel": "^1.0",
"php": "^8.0",
"symfony/console": "^5.3|^6.0|^7.0",
"symfony/var-dumper": "^5.0|^6.0|^7.0"
@@ -13786,7 +13867,7 @@
"ext-gd": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"laravel/octane": "^1.4|^2.0",
- "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8",
+ "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
@@ -13825,9 +13906,9 @@
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
- "source": "https://github.com/laravel/telescope/tree/v5.16.1"
+ "source": "https://github.com/laravel/telescope/tree/5.18.0"
},
- "time": "2025-12-30T17:31:31+00:00"
+ "time": "2026-02-20T19:55:06+00:00"
},
{
"name": "league/uri-components",
@@ -14058,39 +14139,36 @@
},
{
"name": "nunomaduro/collision",
- "version": "v8.8.3",
+ "version": "v8.9.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4"
+ "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4",
- "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
+ "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
"shasum": ""
},
"require": {
- "filp/whoops": "^2.18.1",
- "nunomaduro/termwind": "^2.3.1",
+ "filp/whoops": "^2.18.4",
+ "nunomaduro/termwind": "^2.4.0",
"php": "^8.2.0",
- "symfony/console": "^7.3.0"
+ "symfony/console": "^7.4.4 || ^8.0.4"
},
"conflict": {
- "laravel/framework": "<11.44.2 || >=13.0.0",
- "phpunit/phpunit": "<11.5.15 || >=13.0.0"
+ "laravel/framework": "<11.48.0 || >=14.0.0",
+ "phpunit/phpunit": "<11.5.50 || >=14.0.0"
},
"require-dev": {
- "brianium/paratest": "^7.8.3",
- "larastan/larastan": "^3.4.2",
- "laravel/framework": "^11.44.2 || ^12.18",
- "laravel/pint": "^1.22.1",
- "laravel/sail": "^1.43.1",
- "laravel/sanctum": "^4.1.1",
- "laravel/tinker": "^2.10.1",
- "orchestra/testbench-core": "^9.12.0 || ^10.4",
- "pestphp/pest": "^3.8.2 || ^4.0.0",
- "sebastian/environment": "^7.2.1 || ^8.0"
+ "brianium/paratest": "^7.8.5",
+ "larastan/larastan": "^3.9.2",
+ "laravel/framework": "^11.48.0 || ^12.52.0",
+ "laravel/pint": "^1.27.1",
+ "orchestra/testbench-core": "^9.12.0 || ^10.9.0",
+ "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0",
+ "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0"
},
"type": "library",
"extra": {
@@ -14153,45 +14231,45 @@
"type": "patreon"
}
],
- "time": "2025-11-20T02:55:25+00:00"
+ "time": "2026-02-17T17:33:08+00:00"
},
{
"name": "pestphp/pest",
- "version": "v4.3.2",
+ "version": "v4.4.1",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398"
+ "reference": "f96a1b27864b585b0b29b0ee7331176726f7e54a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/3a4329ddc7a2b67c19fca8342a668b39be3ae398",
- "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/f96a1b27864b585b0b29b0ee7331176726f7e54a",
+ "reference": "f96a1b27864b585b0b29b0ee7331176726f7e54a",
"shasum": ""
},
"require": {
- "brianium/paratest": "^7.16.1",
- "nunomaduro/collision": "^8.8.3",
- "nunomaduro/termwind": "^2.3.3",
+ "brianium/paratest": "^7.19.0",
+ "nunomaduro/collision": "^8.9.0",
+ "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.2.1",
"php": "^8.3.0",
- "phpunit/phpunit": "^12.5.8",
- "symfony/process": "^7.4.4|^8.0.0"
+ "phpunit/phpunit": "^12.5.12",
+ "symfony/process": "^7.4.5|^8.0.5"
},
"conflict": {
"filp/whoops": "<2.18.3",
- "phpunit/phpunit": ">12.5.8",
+ "phpunit/phpunit": ">12.5.12",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
"require-dev": {
- "pestphp/pest-dev-tools": "^4.0.0",
- "pestphp/pest-plugin-browser": "^4.2.1",
+ "pestphp/pest-dev-tools": "^4.1.0",
+ "pestphp/pest-plugin-browser": "^4.3.0",
"pestphp/pest-plugin-type-coverage": "^4.0.3",
- "psy/psysh": "^0.12.18"
+ "psy/psysh": "^0.12.20"
},
"bin": [
"bin/pest"
@@ -14257,7 +14335,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.3.2"
+ "source": "https://github.com/pestphp/pest/tree/v4.4.1"
},
"funding": [
{
@@ -14269,7 +14347,7 @@
"type": "github"
}
],
- "time": "2026-01-28T01:01:19+00:00"
+ "time": "2026-02-17T15:27:18+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -14413,35 +14491,35 @@
},
{
"name": "pestphp/pest-plugin-browser",
- "version": "v4.2.1",
+ "version": "v4.3.0",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest-plugin-browser.git",
- "reference": "0ed837ab7e80e6fc78d36913cc0b006f8819336d"
+ "reference": "48bc408033281974952a6b296592cef3b920a2db"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/0ed837ab7e80e6fc78d36913cc0b006f8819336d",
- "reference": "0ed837ab7e80e6fc78d36913cc0b006f8819336d",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/48bc408033281974952a6b296592cef3b920a2db",
+ "reference": "48bc408033281974952a6b296592cef3b920a2db",
"shasum": ""
},
"require": {
"amphp/amp": "^3.1.1",
- "amphp/http-server": "^3.4.3",
+ "amphp/http-server": "^3.4.4",
"amphp/websocket-client": "^2.0.2",
"ext-sockets": "*",
- "pestphp/pest": "^4.3.1",
+ "pestphp/pest": "^4.3.2",
"pestphp/pest-plugin": "^4.0.0",
"php": "^8.3",
- "symfony/process": "^7.4.3"
+ "symfony/process": "^7.4.5|^8.0.5"
},
"require-dev": {
"ext-pcntl": "*",
"ext-posix": "*",
- "livewire/livewire": "^3.7.3",
- "nunomaduro/collision": "^8.8.3",
- "orchestra/testbench": "^10.8.0",
- "pestphp/pest-dev-tools": "^4.0.0",
+ "livewire/livewire": "^3.7.10",
+ "nunomaduro/collision": "^8.9.0",
+ "orchestra/testbench": "^10.9.0",
+ "pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-laravel": "^4.0",
"pestphp/pest-plugin-type-coverage": "^4.0.3"
},
@@ -14476,7 +14554,7 @@
"unit"
],
"support": {
- "source": "https://github.com/pestphp/pest-plugin-browser/tree/v4.2.1"
+ "source": "https://github.com/pestphp/pest-plugin-browser/tree/v4.3.0"
},
"funding": [
{
@@ -14492,7 +14570,7 @@
"type": "patreon"
}
],
- "time": "2026-01-11T20:32:34+00:00"
+ "time": "2026-02-17T14:54:40+00:00"
},
{
"name": "pestphp/pest-plugin-mutate",
@@ -14886,11 +14964,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.38",
+ "version": "2.1.40",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629",
- "reference": "dfaf1f530e1663aa167bc3e52197adb221582629",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
+ "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"shasum": ""
},
"require": {
@@ -14935,20 +15013,20 @@
"type": "github"
}
],
- "time": "2026-01-30T17:12:46+00:00"
+ "time": "2026-02-23T15:04:35+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "12.5.2",
+ "version": "12.5.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b"
+ "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b",
- "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d",
+ "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d",
"shasum": ""
},
"require": {
@@ -15004,7 +15082,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3"
},
"funding": [
{
@@ -15024,7 +15102,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-24T07:03:04+00:00"
+ "time": "2026-02-06T06:01:44+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -15285,16 +15363,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.5.8",
+ "version": "12.5.12",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889"
+ "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889",
- "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199",
+ "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199",
"shasum": ""
},
"require": {
@@ -15308,8 +15386,8 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
- "phpunit/php-code-coverage": "^12.5.2",
- "phpunit/php-file-iterator": "^6.0.0",
+ "phpunit/php-code-coverage": "^12.5.3",
+ "phpunit/php-file-iterator": "^6.0.1",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
@@ -15320,6 +15398,7 @@
"sebastian/exporter": "^7.0.2",
"sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
+ "sebastian/recursion-context": "^7.0.1",
"sebastian/type": "^6.0.3",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
@@ -15362,7 +15441,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.8"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12"
},
"funding": [
{
@@ -15386,25 +15465,25 @@
"type": "tidelift"
}
],
- "time": "2026-01-27T06:12:29+00:00"
+ "time": "2026-02-16T08:34:36+00:00"
},
{
"name": "rector/rector",
- "version": "2.3.5",
+ "version": "2.3.8",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070"
+ "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070",
- "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
+ "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
- "phpstan/phpstan": "^2.1.36"
+ "phpstan/phpstan": "^2.1.38"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -15438,7 +15517,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.3.5"
+ "source": "https://github.com/rectorphp/rector/tree/2.3.8"
},
"funding": [
{
@@ -15446,7 +15525,7 @@
"type": "github"
}
],
- "time": "2026-01-28T15:22:48+00:00"
+ "time": "2026-02-22T09:45:50+00:00"
},
{
"name": "revolt/event-loop",
@@ -16690,23 +16769,23 @@
},
{
"name": "spatie/laravel-ignition",
- "version": "2.10.0",
+ "version": "2.11.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
- "reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5"
+ "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2abefdcca6074a9155f90b4ccb3345af8889d5f5",
- "reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5",
+ "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/11f38d1ff7abc583a61c96bf3c1b03610a69cccd",
+ "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "illuminate/support": "^11.0|^12.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
"nesbot/carbon": "^2.72|^3.0",
"php": "^8.2",
"spatie/ignition": "^1.15.1",
@@ -16714,10 +16793,10 @@
"symfony/var-dumper": "^7.4|^8.0"
},
"require-dev": {
- "livewire/livewire": "^3.7.0|^4.0",
+ "livewire/livewire": "^3.7.0|^4.0|dev-josh/v3-laravel-13-support",
"mockery/mockery": "^1.6.12",
- "openai-php/client": "^0.10.3",
- "orchestra/testbench": "^v9.16.0|^10.6",
+ "openai-php/client": "^0.10.3|^0.19",
+ "orchestra/testbench": "^v9.16.0|^10.6|^11.0",
"pestphp/pest": "^3.7|^4.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
@@ -16778,7 +16857,7 @@
"type": "github"
}
],
- "time": "2026-01-20T13:16:11+00:00"
+ "time": "2026-02-22T19:14:05+00:00"
},
{
"name": "staabm/side-effects-detector",
@@ -16834,16 +16913,16 @@
},
{
"name": "symfony/http-client",
- "version": "v7.4.5",
+ "version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
+ "reference": "2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
- "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154",
+ "reference": "2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154",
"shasum": ""
},
"require": {
@@ -16911,7 +16990,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v7.4.5"
+ "source": "https://github.com/symfony/http-client/tree/v7.4.6"
},
"funding": [
{
@@ -16931,7 +17010,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-27T16:16:02+00:00"
+ "time": "2026-02-18T09:46:18+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -17013,23 +17092,23 @@
},
{
"name": "ta-tikoma/phpunit-architecture-test",
- "version": "0.8.6",
+ "version": "0.8.7",
"source": {
"type": "git",
"url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
- "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e"
+ "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e",
- "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e",
+ "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8",
+ "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18.0 || ^5.0.0",
"php": "^8.1.0",
"phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0",
- "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
+ "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0"
},
"require-dev": {
@@ -17066,9 +17145,9 @@
],
"support": {
"issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
- "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6"
+ "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7"
},
- "time": "2026-01-30T07:16:00+00:00"
+ "time": "2026-02-17T17:25:14+00:00"
},
{
"name": "theseer/tokenizer",
diff --git a/config/database.php b/config/database.php
index 79da0eaf7..a5e0ba703 100644
--- a/config/database.php
+++ b/config/database.php
@@ -49,7 +49,7 @@
'search_path' => 'public',
'sslmode' => 'prefer',
'options' => [
- PDO::PGSQL_ATTR_DISABLE_PREPARES => env('DB_DISABLE_PREPARES', false),
+ (defined('Pdo\Pgsql::ATTR_DISABLE_PREPARES') ? \Pdo\Pgsql::ATTR_DISABLE_PREPARES : \PDO::PGSQL_ATTR_DISABLE_PREPARES) => env('DB_DISABLE_PREPARES', false),
],
],
diff --git a/database/factories/EnvironmentFactory.php b/database/factories/EnvironmentFactory.php
new file mode 100644
index 000000000..98959197d
--- /dev/null
+++ b/database/factories/EnvironmentFactory.php
@@ -0,0 +1,16 @@
+ fake()->unique()->word(),
+ 'project_id' => 1,
+ ];
+ }
+}
diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php
new file mode 100644
index 000000000..0b2b72b8a
--- /dev/null
+++ b/database/factories/ProjectFactory.php
@@ -0,0 +1,16 @@
+ fake()->unique()->company(),
+ 'team_id' => 1,
+ ];
+ }
+}
diff --git a/database/factories/ServiceFactory.php b/database/factories/ServiceFactory.php
new file mode 100644
index 000000000..62c5f7cda
--- /dev/null
+++ b/database/factories/ServiceFactory.php
@@ -0,0 +1,19 @@
+ fake()->unique()->word(),
+ 'destination_type' => \App\Models\StandaloneDocker::class,
+ 'destination_id' => 1,
+ 'environment_id' => 1,
+ 'docker_compose_raw' => 'version: "3"',
+ ];
+ }
+}
diff --git a/database/factories/StandaloneDockerFactory.php b/database/factories/StandaloneDockerFactory.php
new file mode 100644
index 000000000..d37785189
--- /dev/null
+++ b/database/factories/StandaloneDockerFactory.php
@@ -0,0 +1,18 @@
+ fake()->uuid(),
+ 'name' => fake()->unique()->word(),
+ 'network' => 'coolify',
+ 'server_id' => 1,
+ ];
+ }
+}
diff --git a/database/migrations/2025_11_17_145255_add_comment_to_environment_variables_table.php b/database/migrations/2025_11_17_145255_add_comment_to_environment_variables_table.php
new file mode 100644
index 000000000..abbae3573
--- /dev/null
+++ b/database/migrations/2025_11_17_145255_add_comment_to_environment_variables_table.php
@@ -0,0 +1,36 @@
+string('comment', 256)->nullable();
+ });
+
+ Schema::table('shared_environment_variables', function (Blueprint $table) {
+ $table->string('comment', 256)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('comment');
+ });
+
+ Schema::table('shared_environment_variables', function (Blueprint $table) {
+ $table->dropColumn('comment');
+ });
+ }
+};
diff --git a/database/migrations/2026_02_26_163035_add_stripe_refunded_at_to_subscriptions_table.php b/database/migrations/2026_02_26_163035_add_stripe_refunded_at_to_subscriptions_table.php
new file mode 100644
index 000000000..76420fb5c
--- /dev/null
+++ b/database/migrations/2026_02_26_163035_add_stripe_refunded_at_to_subscriptions_table.php
@@ -0,0 +1,25 @@
+timestamp('stripe_refunded_at')->nullable()->after('stripe_past_due');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->dropColumn('stripe_refunded_at');
+ });
+ }
+};
diff --git a/openapi.json b/openapi.json
index 9bfcd8442..69f5ef53d 100644
--- a/openapi.json
+++ b/openapi.json
@@ -11404,6 +11404,10 @@
"real_value": {
"type": "string"
},
+ "comment": {
+ "type": "string",
+ "nullable": true
+ },
"version": {
"type": "string"
},
diff --git a/openapi.yaml b/openapi.yaml
index 76ebe9e96..fab3df54e 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -7241,6 +7241,9 @@ components:
type: string
real_value:
type: string
+ comment:
+ type: string
+ nullable: true
version:
type: string
created_at:
diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php
index 73939092e..e77b52076 100644
--- a/resources/views/components/modal-confirmation.blade.php
+++ b/resources/views/components/modal-confirmation.blade.php
@@ -35,7 +35,7 @@
$skipPasswordConfirmation = shouldSkipPasswordConfirmation();
if ($temporaryDisableTwoStepConfirmation) {
$disableTwoStepConfirmation = false;
- $skipPasswordConfirmation = false;
+ // Password confirmation requirement is not affected by temporary two-step disable
}
// When password step is skipped, Step 2 becomes final - change button text from "Continue" to "Confirm"
$effectiveStep2ButtonText = ($skipPasswordConfirmation && $step2ButtonText === 'Continue') ? 'Confirm' : $step2ButtonText;
@@ -59,6 +59,7 @@
confirmWithPassword: @js($confirmWithPassword && !$skipPasswordConfirmation),
submitAction: @js($submitAction),
dispatchAction: @js($dispatchAction),
+ submitting: false,
passwordError: '',
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
dispatchEvent: @js($dispatchEvent),
@@ -70,6 +71,7 @@
this.step = this.initialStep;
this.deleteText = '';
this.password = '';
+ this.submitting = false;
this.userConfirmationText = '';
this.selectedActions = @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all());
$wire.$refresh();
@@ -320,8 +322,8 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
@endif
Your session has expired. Please log in again to continue.
-localhost, log in there first to configure your FQDN.Eligible for a full refund — {{ $refundDaysRemaining }} days remaining.
+ @elseif ($refundAlreadyUsed) +Refund already processed. Each team is eligible for one refund only.
+ @endif + @if (currentTeam()->subscription->stripe_cancel_at_period_end) +Your subscription is set to cancel at the end of the billing period.
+ @endif + + +Open the following link, navigate to the button and pay your unpaid/past due subscription. @@ -34,18 +31,20 @@
Dynamic pricing based on the number of servers you connect.
+ +- Dynamic pricing based on the number of servers you connect. -
-- - $5 - base price - +
+ + + $3 per additional server, billed monthly (+VAT) + + + + $2.7 per additional server, billed annually (+VAT) + +
- - $4 - base price - - -- - $3 - per additional servers billed monthly (+VAT) - + {{-- Subscribe Button --}} +
You need to bring your own servers from any cloud provider (Hetzner, DigitalOcean, AWS, etc.) or connect any device running a supported OS.
+Need official support for your self-hosted instance? Contact Us