Merge pull request #946 from tdurieux/feat/one-time-billing

feat: let authors pay once per paper instead of subscribing
This commit is contained in:
Thomas Durieux authored and GitHub committed 2026-11-13 03:32:52 +01:00
commit 3408a6ced9
29 files changed
+1423 -273

No files matched your search

+51 -14
View File
@@ -4,12 +4,22 @@ Billing is opt-in. `BILLING_ENABLED=false` preserves self-hosted behavior. This
## Offers
Configure four EUR prices in Stripe. The amounts below are suggested launch prices, not hard-coded charges:
Configure these EUR prices in Stripe. The amounts below are suggested prices, not hard-coded charges:
- Author monthly: €5/month, recurring monthly.
- Author annual: €49/year, recurring yearly.
- Author review pass: €15 once, one existing repository for 180 days from checkout creation.
- Conference review package: €199 once, up to 100 repositories during a fixed review window of at most 180 days.
- Author annual: €49/year, recurring yearly. In the first nine days of paid hosting, 6 of 22 subscribers chose it and it brought 79% of revenue, so it keeps its price; it is also easy to expense on a grant.
- Live updates (`author_pass`): €4 once. Automatic refresh and a separate storage allowance for one existing repository for 180 days from checkout creation.
- Extended expiration (`repo_extend`): €2 once. One existing repository can be scheduled to expire up to one year after purchase. Purchases stack: each one adds a year after the latest earlier extension.
- Never expire (`repo_never`): €10 once. It costs more than Extended expiration because it is the only purchase that keeps storage in use indefinitely. One existing repository can use Never expire without a subscription.
- Extra repository (`repo_slot`): €2 once. One more repository beyond the free allowance, with its own `PAID_PASS_STORAGE` allowance, for as long as the repository exists. It does not count toward the free allowance; buying it for a repository already counted removes that repository from the free ledger. It does not include automatic updates.
- Paper pack (`repo_pack`): €5 once. One checkout for one repository that grants Extra repository, Live updates and Extended expiration together. It is stored as three grants sharing the payment, so a refund or dispute revokes all three.
- Conference review packages, one per venue size, each for a fixed review window of at most 180 days with automatic updates. With billing enabled they are the only plans for new conferences. The size is chosen at creation and cannot change.
- Workshop (`conference_workshop`, `STRIPE_CONFERENCE_SMALL_PRICE`): €49 once, 25 repositories, 10 GiB (about €1.96 per paper).
- Conference (`conference`, `STRIPE_CONFERENCE_PRICE`): €149 once, 100 repositories, 50 GiB (about €1.49 per paper). The id is unchanged from the former single package.
- Large conference (`conference_large`, `STRIPE_CONFERENCE_LARGE_PRICE`): €399 once, 400 repositories, 200 GiB (about €1 per paper).
A tier without a configured price is not offered.
Most authors use the service for one paper at a time, a few times a year. The one-time repository add-ons are the main offer; the subscriptions suit heavy users. Extended expiration and Never expire do not add storage: the repository keeps using its owner's allowance (or its pass or conference allowance).
Checkout uses the configured Stripe Price ID. The settings page displays its actual amount and cadence. Do not configure trials, metered prices, tiered prices, adjustable quantities, or subscription changes to unrelated products in the customer portal. New subscriptions need a paid invoice before access is granted. A failed renewal does not extend paid time. Cancellation at period end keeps access until that period ends; immediate cancellation ends new paid usage immediately.
@@ -19,8 +29,8 @@ Default capacities, all measured in source-file bytes:
- Free account: 5 repositories, 2 GiB total.
- Author subscription: 20 repositories, 10 GiB total, and never-expiring repositories while subscribed.
- Repository pass: 1 repository, 2 GiB.
- Conference: 100 repositories, 50 GiB total.
- Live updates: 1 repository, 2 GiB.
- Conference packages: 25 repositories / 10 GiB, 100 / 50 GiB, and 400 / 200 GiB.
These are admission limits, not a measure or guarantee of physical disk consumption. Original files, anonymized caches, metadata and backups need additional capacity. Review production usage before enabling billing. File-size safeguards and request throttles still apply to paid users. Reviewers do not need accounts or payment.
@@ -35,16 +45,25 @@ STRIPE_SECRET_KEY=<Stripe secret key>
STRIPE_WEBHOOK_SECRET=<Stripe webhook signing secret>
STRIPE_AUTHOR_MONTHLY_PRICE=<monthly recurring EUR price ID>
STRIPE_AUTHOR_ANNUAL_PRICE=<annual recurring EUR price ID>
STRIPE_AUTHOR_PASS_PRICE=<one-time EUR price ID>
STRIPE_AUTHOR_PASS_PRICE=<one-time EUR price ID, Live updates>
STRIPE_REPO_EXTEND_PRICE=<one-time EUR price ID, Extended expiration>
STRIPE_REPO_NEVER_PRICE=<one-time EUR price ID, Never expire>
STRIPE_REPO_SLOT_PRICE=<one-time EUR price ID, Extra repository>
STRIPE_REPO_PACK_PRICE=<one-time EUR price ID, Paper pack>
STRIPE_CONFERENCE_SMALL_PRICE=<one-time EUR price ID>
STRIPE_CONFERENCE_PRICE=<one-time EUR price ID>
STRIPE_CONFERENCE_LARGE_PRICE=<one-time EUR price ID>
# Optional: retired price IDs still used by existing purchases
STRIPE_LEGACY_PRICES=price_old_annual:author_annual
BILLING_AUTOMATIC_TAX=false
```
Price IDs can be omitted to hide offers. Use test-mode keys and prices for staging. `BILLING_ORIGIN` must be the application's canonical HTTPS origin; HTTP is accepted only for localhost development. Redirects never use request-supplied hosts or return URLs. Enable and configure Stripe Tax before setting `BILLING_AUTOMATIC_TAX=true`; otherwise configure the appropriate tax treatment in Stripe before launch. Receipts, seller details and portal settings also need configuration in Stripe.
Capacity overrides are positive integers:
Capacity and expiration overrides are positive integers:
```text
FREE_EXPIRATION_MONTHS=5
FREE_AUTHOR_REPOSITORIES=5
FREE_AUTHOR_STORAGE=2147483648
PAID_AUTHOR_REPOSITORIES=20
@@ -52,9 +71,21 @@ PAID_AUTHOR_STORAGE=10737418240
PAID_PASS_STORAGE=2147483648
PAID_CONFERENCE_REPOSITORIES=100
PAID_CONFERENCE_STORAGE=53687091200
PAID_CONFERENCE_SMALL_REPOSITORIES=25
PAID_CONFERENCE_SMALL_STORAGE=10737418240
PAID_CONFERENCE_LARGE_REPOSITORIES=400
PAID_CONFERENCE_LARGE_STORAGE=214748364800
```
Changing these values changes the allowance of existing plans too. Keep existing Price IDs configured for as long as subscriptions using them remain active. Plan and conference review-window changes are intentionally locked in this first version.
Changing these values changes the allowance of existing plans too. Plan and conference review-window changes are intentionally locked in this first version.
### Changing a price
Existing subscriptions and purchases keep the Stripe price they were sold at. The application maps a price to its plan through the configured price, then `STRIPE_LEGACY_PRICES`, then a registry of prices it has seen: every configured price is recorded at startup, and every price is recorded when a checkout is created. To replace a price:
1. Deploy this version while the old price is still configured, so startup records it.
2. Add the old price to `STRIPE_LEGACY_PRICES` (for example `price_old_annual:author_annual`). This is explicit and also covers prices the registry never saw.
3. Set the new price ID and restart. New checkouts use the new price; existing subscribers keep theirs, including on renewal and cancellation webhooks.
Configure the Stripe customer portal for payment-method updates, invoice downloads and cancellation. Disable arbitrary subscription product switches. Configure a snapshot webhook at `/api/billing/webhook` using API version `2026-10-28.endive`, matching the installed Stripe SDK, for:
@@ -74,11 +105,11 @@ The webhook is mounted before JSON parsing so Stripe verifies the original reque
## Lifecycle and migration
New paid conferences remain `pending_payment` until a verified checkout activates them. Collect billing addresses in Stripe. Historic conference plans remain editable and keep their existing status, but are no longer offered to new conferences. There is no automatic charge for historical estimated usage.
New paid conferences remain `pending_payment` until a verified checkout activates them. Collect billing addresses in Stripe. With billing enabled, the review package is the only conference plan; the free plan is offered only when billing is disabled (and no plan is offered if `STRIPE_CONFERENCE_PRICE` is missing). Historic free conferences remain editable and keep their existing status and behavior. `GET /api/conferences/:id` returns `hosting: true` while a paid conference is active, so the anonymize form can tell authors that the conference pays for their repository. There is no automatic charge for historical estimated usage.
Existing repositories are not deleted or hidden on rollout or subscription expiry. New repositories are marked `billingManaged`; their file trees must fit an allowance before publication. A repository that fails this check remains available in its owner's dashboard for upgrading and retrying. Buy a pass by entering that repository's anonymized ID in Settings. Automatic refresh pauses for billing-managed repositories on the free allowance, including after paid access expires. Owners can still request a refresh within that allowance. Existing review links retain their configured expiration dates. This is not a guarantee of perpetual hosting.
Existing repositories are not deleted or hidden on rollout or subscription expiry. New repositories are marked `billingManaged`; their file trees must fit an allowance before publication. A repository that fails this check remains available in its owner's dashboard for upgrading and retrying. Buy an add-on from the repository's edit page, or by entering its anonymized ID in Settings. Add-ons can also be bought for an unused repository ID: when the free allowance is full, the create form offers Extra repository and returns to the saved draft after checkout. Automatic refresh pauses for billing-managed repositories on the free allowance, including after paid access expires. Owners can still request a refresh within that allowance. Existing review links retain their configured expiration dates. This is not a guarantee of perpetual hosting.
Allowances are selected in order: paid conference, repository pass, author subscription, free account. MongoDB quota ledgers atomically reserve count and source-file bytes. They initially count existing repositories using stored size metadata. Reservations never shrink during refresh, so a concurrent or failed build cannot release capacity still in use. Confirmed removed repositories release reservations on the next admission. Expired repositories and orphan reservations remain reserved until operator reconciliation. This deliberately favors conservative capacity accounting over overselling. The account usage display uses reserved capacity once a ledger exists. A moved repository can retain conservative reservations in its previous scope until removal.
Allowances are selected in order: paid conference, Live updates or Extra repository, author subscription, free account. Repository add-ons only apply to the account that bought them. Extended expiration and Never expire grants share the repository scope but are skipped here. MongoDB quota ledgers atomically reserve count and source-file bytes. They initially count existing repositories using stored size metadata. Reservations never shrink during refresh, so a concurrent or failed build cannot release capacity still in use. Confirmed removed repositories release reservations on the next admission. Expired repositories and orphan reservations remain reserved until operator reconciliation. This deliberately favors conservative capacity accounting over overselling. The account usage display uses reserved capacity once a ledger exists. A moved repository can retain conservative reservations in its previous scope until removal.
For billing-managed repositories, on-demand recovery and ZIP downloads from truncated trees are rejected with `repository_tree_incomplete`. This prevents serving unaccounted files beyond the indexed tree. A complete source tree is required; refresh after reducing repository size or excluded content upstream.
@@ -110,11 +141,17 @@ The free defaults are 5 repositories and 2 GiB, revised from the initial 10-repo
### Never-expiring repositories
Monthly and annual author subscribers can choose Never expire. This requires the repository owner's subscription even when a conference pays its storage allowance. A coauthor subscription or a one-time review pass does not unlock it.
Monthly and annual author subscribers can choose Never expire. This requires the repository owner's subscription even when a conference pays its storage allowance, or a Never expire add-on bought for that repository. A coauthor subscription or Live updates does not unlock it.
Newly selected Never expire repositories store a billing deadline 90 days after the paid-through date. The five-minute maintenance job extends that deadline on renewal and changes overdue repositories to ordinary removal expiration. Cancellation does not shorten the paid-through period. Revocation does not extend a previously recorded deadline. Renewal during the grace period preserves the repository; after expiration, the author must renew and explicitly reactivate it.
Legacy never-expiring repositories have no billing deadline and remain unchanged. Disabling billing disables this reconciliation. Free users can choose a dated expiration within one year and extend it manually. Never expire does not exempt a repository from quotas, abuse controls, or service availability.
A Never expire add-on also stores a billing deadline, 90 days ahead. Maintenance moves it forward about once a week while the add-on is valid, so a full refund or dispute ends the repository after the grace period like a lapsed subscription.
Legacy never-expiring repositories have no billing deadline and remain unchanged. Disabling billing disables this reconciliation.
### Dated expiration limits
Free users can choose a dated expiration up to `FREE_EXPIRATION_MONTHS` (default 5) months ahead and extend it again later. The dashboard's Extend action moves the date forward by up to 6 months, capped at this limit. Author subscribers keep a one-year limit. An Extended expiration add-on raises the limit to its paid date. A repository in a conference can expire as late as that conference's end date. An already saved later date stays valid, so lowering the limit does not block edits to existing repositories. Without billing, the limit stays at one year. Never expire does not exempt a repository from quotas, abuse controls, or service availability.
Real sandbox validation and repeatable API test instructions are recorded in [Stripe sandbox validation](stripe-sandbox-validation.md).
+2 -2
View File
@@ -1,8 +1,8 @@
{
"core.min.js": "core.d23af1c652.min.js",
"vendor.min.js": "vendor.3f99749002.min.js",
"vendor.min.js": "vendor.d5b4d04371.min.js",
"mermaid.min.js": "mermaid.a7356d5ed1.min.js",
"all.min.css": "all.c690a01ace.min.css",
"all.min.css": "all.8c4e8ecba6.min.css",
"markdown.min.js": "markdown.6c8dfb7b1d.min.js",
"pdf.min.js": "pdf.4d9cc6ff49.min.js",
"notebook.min.js": "notebook.c3144c18ae.min.js",
+1 -1
View File
@@ -6,4 +6,4 @@
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unsLine truncated
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
+209 -1
View File
@@ -3218,6 +3218,34 @@ code {
.paper-plan-desc { font-size: 13px; color: var(--ink-soft); }
.paper-plan-desc ul { padding-left: 18px; margin: 0; }
/* New conference: the single paid review package */
.conference-package { padding: 20px 22px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; margin: 0 0 12px; }
.conference-package-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin: 0 0 14px; }
.conference-package-head h3 { font-size: 1.05rem; font-weight: 600; margin: 0 0 2px; }
.conference-package-head p { margin: 0; font-size: 13px; color: var(--ink-soft); }
.conference-package-price { display: flex; flex-direction: column; align-items: flex-end; flex: none; }
.conference-package-price strong { font-size: 1.6rem; font-weight: 600; line-height: 1.1; color: var(--color); font-variant-numeric: tabular-nums; }
.conference-package-price small { font-size: 12px; color: var(--ink-muted); }
.conference-package .billing-offer-list { list-style: none; margin: 0 0 14px; padding: 0; font-size: 13px; line-height: 1.5; }
.conference-package .billing-offer-list li { position: relative; padding-left: 22px; margin: 0 0 6px; }
.conference-package .billing-offer-list li::before { content: ""; position: absolute; left: 3px; top: 6px; width: 10px; height: 5px; border-left: 1.5px solid var(--ink-muted); border-bottom: 1.5px solid var(--ink-muted); transform: rotate(-45deg); }
.conference-package h3 { font-size: 0.95rem; font-weight: 600; margin: 0 0 10px; }
.conference-tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin: 0 0 12px; }
.conference-tier { position: relative; display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 16px 18px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; cursor: pointer; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.conference-tier:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border-color)); }
.conference-tier.is-selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; background: var(--accent-soft); }
.conference-tier:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.paper-page .conference-tier input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.conference-tier-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.conference-tier-head strong { font-size: 15px; font-weight: 600; color: var(--color); }
.conference-tier-pill { font-size: 11px; font-weight: 500; padding: 1px 8px; border-radius: 999px; color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); white-space: nowrap; }
.conference-tier-who { font-size: 13px; color: var(--ink-soft); }
.conference-tier-price { margin-top: 8px; font-size: 12px; color: var(--ink-muted); }
.conference-tier-price strong { font-size: 1.5rem; font-weight: 600; color: var(--color); font-variant-numeric: tabular-nums; margin-right: 2px; }
.conference-tier-size { font-size: 13px; color: var(--color); }
.conference-tier-per { font-size: 12px; color: var(--ink-muted); }
.conference-package-note { margin: 0; padding-top: 12px; border-top: 1px solid var(--border-color); font-size: 12px; color: var(--ink-muted); }
.paper-settings-copy { color: var(--ink-soft); font-size: 13px; margin: 0 0 14px; }
.anonymize-section-title {
@@ -8784,7 +8812,10 @@ body.dark-mode .markdown-body .gh-light-mode-only { display: none; }
.hosting-property { margin-bottom: 0; }
.hosting-property label, .hosting-date .paper-field-label { font-size: 12px; font-weight: 400; margin: 0; }
.hosting-date .invalid-feedback { grid-column: 1/-1; }
.anonymize-page .conference-field { border-top: 1px solid var(--border-color); padding: 16px 0; margin: 0; }
.anonymize-page .conference-field { border-bottom: 1px solid var(--border-color); padding: 0 0 16px; margin: 0 0 14px; }
.anonymize-page .conference-hosting { margin: 8px 0 0; padding: 8px 12px; border-radius: 8px; font-size: 13px; line-height: 1.45; color: var(--ink-soft); background: color-mix(in srgb, var(--ink-muted) 8%, transparent); }
.anonymize-page .conference-hosting.is-covered { color: var(--color); background: var(--accent-soft); }
.anonymize-page .conference-hosting a { font-weight: 600; }
.subscription-choice { padding: 16px; background: var(--paper-bg-alt); border-radius: 6px; margin-top: 4px; }
.subscription-choice h3 { font-size: 13px; font-weight: 600; margin: 0 0 10px; }
.billing-cycle { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; border: 0; padding: 0; margin: 0 0 12px; min-width: 0; }
@@ -8909,6 +8940,9 @@ body.dark-mode .preserve-dialog { background: #242320; color: #faf9f6; }
.settings-cycles label { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border-color); background: var(--paper-card); border-radius: 5px; padding: 12px; font-size: 13px; margin: 0; cursor: pointer; flex: 1; }
.settings-cycles label.selected { border-color: var(--accent); background: var(--accent-soft); }
.settings-cycles input { accent-color: var(--accent); }
.settings-addons { flex-direction: column; }
.settings-addons label { align-items: flex-start; }
.settings-addons small { color: var(--ink-muted); font-size: 12px; }
.settings-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.settings-notice { background: var(--paper-bg-alt); border: 1px solid var(--border-color); border-radius: 5px; padding: 14px 16px; margin: 16px 0; font-size: 13px; }
.settings-notice p { margin: 6px 0 12px; }
@@ -8919,6 +8953,16 @@ body.dark-mode .preserve-dialog { background: #242320; color: #faf9f6; }
.settings-purchase { padding: 12px 0; border-bottom: 1px solid var(--border-color); font-size: 13px; overflow-wrap: anywhere; }
.settings-purchase:last-child { border: 0; }
.settings-purchase > span { display: block; color: var(--ink-muted); margin-top: 4px; }
.settings-purchase-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.settings-purchase-parts { margin: 6px 0 0; padding-left: 18px; font-size: 12px; color: var(--ink-muted); }
.settings-status { display: inline-block; font-size: 12px; font-weight: 500; line-height: 18px; border-radius: 999px; padding: 1px 9px; background: var(--paper-bg-alt); color: var(--ink-muted); white-space: nowrap; }
.settings-status.is-active { background: var(--accent-soft); color: var(--accent); box-shadow: inset 0 0 0 1px rgba(31, 106, 100, .3); }
.settings-status.is-overdue, .settings-status.is-refunded, .settings-status.is-disputed { background: rgba(185, 28, 28, .08); color: #b91c1c; }
body.dark-mode .settings-status.is-overdue, body.dark-mode .settings-status.is-refunded, body.dark-mode .settings-status.is-disputed { background: rgba(248, 113, 113, .16); color: #f87171; }
.settings-badge { display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 600; letter-spacing: .02em; border-radius: 999px; padding: 1px 7px; background: var(--accent-soft); border: 1px solid var(--accent); vertical-align: middle; }
.user-settings .settings-comparison th:first-child { width: 22%; }
.settings-comparison thead .settings-badge { display: table; margin: 6px 0 0; white-space: nowrap; }
.user-settings .settings-subhead { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted); margin: 18px 0 4px; }
.user-settings .paper-account-card { padding: 24px 0; border: 0; background: none; }
.user-settings .paper-danger-zone { margin-top: 24px; }
@media (max-width: 767px) {
@@ -11411,6 +11455,8 @@ mark.md-find.is-current { background: rgba(232,160,30,.85) !important; box-shado
/* ===== Round 64 ===== */
/* themed checkbox everywhere in admin + dashboard */
.paper-page input[type="checkbox"] { -webkit-appearance: none; appearance: none; width: 16px !important; height: 16px !important; margin: 0; border: 1.5px solid #A39D92; border-radius: 4px; background: var(--paper-card); display: inline-grid; place-content: center; cursor: pointer; vertical-align: middle; }
/* Settings checkboxes sit in the 28px gutter left of their label; the reset above zeroes that offset. */
.paper-settings-section .form-check > input.form-check-input[type="checkbox"] { position: absolute; margin: 3px 0 0 -28px; }
.paper-page input[type="checkbox"]::before { content: ""; width: 9px; height: 9px; transform: scale(0); background: #fff; -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat; }
.paper-page input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
.paper-page input[type="checkbox"]:checked::before { transform: scale(1); }
@@ -12559,3 +12605,165 @@ input.form-control[type="date"]::-webkit-datetime-edit { padding: 0; }
.paper-table { overflow: visible !important; }
.paper-table > .paper-table-head { border-top-left-radius: inherit; border-top-right-radius: inherit; }
.paper-table > :last-child { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
/* Hosting & billing tab */
.user-settings #settings-billing > .settings-scope { margin-bottom: 14px; }
.user-settings .billing-summary { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; padding: 24px; }
.user-settings .billing-summary-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
.user-settings .billing-eyebrow { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-muted); margin: 0 0 6px; }
.user-settings .billing-plan-name { font-size: 24px; font-weight: 600; line-height: 1.2; letter-spacing: -.01em; margin: 0; }
.user-settings .billing-plan-meta { font-size: 13px; color: var(--ink-muted); margin: 6px 0 0; }
.user-settings .billing-summary .quota-track { height: 6px; border-radius: 3px; background: var(--border-color); }
.user-settings .billing-summary .quota-track .quota-fill { border-radius: 3px; }
.user-settings .billing-summary .quota-unlimited-tag { display: none; }
.user-settings .billing-summary .quota-usage { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; }
.user-settings .billing-summary .quota-item:has(.quota-unlimited-tag) { display: none; }
.user-settings .billing-summary .settings-notice { margin: 18px 0 0; }
.user-settings .billing-footnote { font-size: 12px; line-height: 1.6; color: var(--ink-muted); margin: 16px 0 0; }
.user-settings .billing-section { margin-top: 44px; }
.user-settings .billing-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.user-settings .billing-section-title { font-size: 18px; font-weight: 600; letter-spacing: -.005em; margin: 0 0 14px; }
.user-settings .billing-section-title:focus { outline: none; }
.user-settings .billing-section-head .settings-text-button { font-size: 12px; padding: 0; min-height: 0; }
.user-settings .billing-offers { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; padding-top: 10px; }
.user-settings .billing-offer { position: relative; display: flex; flex-direction: column; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; padding: 24px 22px 22px; min-width: 0; }
.user-settings .billing-offer.is-recommended { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.user-settings .billing-offer-pill { position: absolute; top: -10px; left: 20px; font-size: 11px; font-weight: 600; line-height: 20px; padding: 0 9px; border-radius: 999px; background: var(--accent); color: #fff; }
body.dark-mode .user-settings .billing-offer-pill { color: #1A1815; }
.user-settings .billing-offer h3 { font-size: 16px; font-weight: 600; margin: 0; }
.user-settings .billing-offer-price { margin: 8px 0 16px; color: var(--ink-muted); font-size: 13px; line-height: 40px; height: 40px; }
.user-settings .billing-offer-price strong { font-size: 32px; font-weight: 600; letter-spacing: -.02em; color: var(--color); vertical-align: -2px; }
.user-settings .billing-offer-list { list-style: none; margin: 0 0 20px; padding: 0; font-size: 13px; line-height: 1.5; }
.user-settings .billing-offer-list li { position: relative; padding-left: 22px; margin: 0 0 6px; }
.user-settings .billing-offer-list li::before { content: ""; position: absolute; left: 3px; top: 6px; width: 10px; height: 5px; border-left: 1.5px solid var(--ink-muted); border-bottom: 1.5px solid var(--ink-muted); transform: rotate(-45deg); }
.user-settings .billing-offer > .btn { margin-top: auto; width: 100%; }
.user-settings .billing-addon-list { display: flex; flex-direction: column; gap: 2px; margin: 0 -8px 20px; }
.user-settings .billing-addon { display: grid; grid-template-columns: 14px minmax(0, 1fr) auto; gap: 10px; align-items: start; margin: 0; padding: 7px 8px; border-radius: 6px; cursor: pointer; font-weight: 400; }
.user-settings .billing-addon:hover, .user-settings .billing-addon.is-on { background: var(--paper-bg-alt); }
.user-settings .billing-addon input { margin: 3px 0 0; accent-color: var(--accent); }
.user-settings .billing-addon strong { display: block; font-size: 13px; font-weight: 600; line-height: 1.35; }
.user-settings .billing-addon small { display: block; font-size: 12px; color: var(--ink-muted); }
.user-settings .billing-addon-price { font-size: 13px; font-weight: 600; white-space: nowrap; line-height: 1.35; }
.billing-toggle { display: inline-flex; flex-shrink: 0; padding: 2px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--paper-bg-alt); }
.billing-toggle button { border: 0; background: none; border-radius: 6px; padding: 3px 10px; font-size: 12px; line-height: 18px; color: var(--ink-muted); cursor: pointer; }
.billing-toggle button.is-on { background: var(--paper-card); color: var(--color); font-weight: 600; box-shadow: 0 1px 2px rgba(0, 0, 0, .1); }
.billing-toggle button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.billing-toggle-save { margin-left: 5px; font-weight: 600; color: var(--accent); }
.user-settings .billing-offer-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; height: 26px; margin-bottom: 4px; }
.user-settings .billing-offer-head .billing-eyebrow { margin: 0; }
.user-settings .btn-ink:disabled, .user-settings .btn-ink.disabled { background: var(--paper-bg-alt); border-color: var(--border-color); color: var(--ink-muted); opacity: 1; cursor: not-allowed; }
.user-settings .billing-buy { margin-top: 10px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; padding: 24px; }
.user-settings .billing-buy-info .billing-offer-price { margin: 4px 0 8px; }
.user-settings .billing-buy-form { display: flex; flex-direction: column; gap: 10px; padding-left: 32px; border-left: 1px solid var(--border-color); align-self: center; }
.user-settings .billing-buy-form .paper-field-label { margin: 0; }
.user-settings .billing-buy-form .btn { width: 100%; }
.user-settings .billing-buy-form .billing-footnote { margin: 0; }
.user-settings .billing-buy:focus { outline: none; }
.user-settings .billing-back { font-size: 13px; padding: 4px 10px; min-height: 0; margin-bottom: 16px; border-color: transparent; background: transparent; margin-left: -10px; }
.user-settings .billing-back:hover { background: var(--paper-bg-alt); }
.user-settings .billing-buy h3 { font-size: 18px; font-weight: 600; margin: 0; }
.user-settings .billing-buy h3 span { font-size: 14px; font-weight: 400; color: var(--ink-muted); margin-left: 8px; }
.user-settings .billing-buy-desc { font-size: 13px; line-height: 1.55; color: var(--ink-muted); margin: 0; }
.user-settings .billing-buy-row { display: flex; gap: 12px; align-items: center; }
.user-settings .billing-buy-row select { flex: 1; min-width: 0; }
.user-settings .billing-buy-row .btn { flex-shrink: 0; }
.user-settings .billing-compare { margin-top: 16px; font-size: 13px; }
.user-settings .billing-compare summary { cursor: pointer; color: var(--accent); width: max-content; list-style: none; display: flex; align-items: center; gap: 6px; }
.user-settings .billing-compare summary::-webkit-details-marker { display: none; }
.user-settings .billing-compare summary::before { content: ""; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform .15s; margin-right: 2px; }
.user-settings .billing-compare[open] summary::before { transform: rotate(45deg); }
.user-settings .billing-compare-card { margin-top: 12px; padding: 6px 20px 8px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; }
.user-settings .billing-compare .settings-comparison { width: 100%; margin: 0; font-size: 13px; border-collapse: separate; border-spacing: 0; }
.user-settings .billing-compare .settings-comparison td.is-recommended { border-bottom-color: transparent; }
.user-settings .billing-compare .settings-comparison th, .user-settings .billing-compare .settings-comparison td { padding: 10px 14px; border-bottom: 1px solid var(--border-color); }
.user-settings .billing-compare .settings-comparison tr > :first-child { padding-left: 0; }
.user-settings .billing-compare .settings-comparison thead th { font-size: 14px; font-weight: 600; vertical-align: bottom; }
.user-settings .billing-compare .settings-comparison thead th small { display: block; font-size: 12px; font-weight: 400; color: var(--ink-muted); margin-top: 2px; }
.user-settings .billing-compare .settings-comparison thead th.is-recommended { border-radius: 8px 8px 0 0; }
.user-settings .billing-compare .settings-comparison tbody tr:last-child td.is-recommended { border-radius: 0 0 8px 8px; }
.user-settings .billing-compare .settings-comparison tbody tr:last-child > * { border-bottom: 0; }
.user-settings .billing-compare .settings-comparison th[scope=row] { color: var(--ink-muted); font-weight: 400; }
.user-settings .billing-compare .settings-comparison .is-recommended { background: var(--accent-soft); }
.user-settings .billing-empty { font-size: 13px; color: var(--ink-muted); margin: 0; }
.user-settings .billing-history { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border-color); }
.user-settings .billing-history-head, .user-settings .billing-history-row { display: grid; grid-template-columns: minmax(0, 1fr) 160px 120px 88px; gap: 16px; }
.user-settings .billing-history-head { padding: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-muted); }
.user-settings .billing-history-head span:nth-child(n+3) { text-align: right; }
.user-settings .billing-history-row { align-items: baseline; padding: 14px 0; border-bottom: 1px solid var(--border-color); font-size: 13px; }
.user-settings .billing-history-main strong { display: block; font-weight: 600; overflow-wrap: anywhere; }
.user-settings .billing-history-main strong a { font-weight: 500; }
.user-settings .billing-history-main > span { display: block; font-size: 12px; color: var(--ink-muted); margin-top: 3px; }
.user-settings .billing-history-term { color: var(--ink-muted); }
.user-settings .billing-history-term:empty::before { content: "—"; }
.user-settings .billing-history-paid { text-align: right; }
.user-settings .billing-history-paid small { display: block; color: var(--ink-muted); font-size: 12px; margin-top: 2px; }
.user-settings .billing-history-status { text-align: right; }
.user-settings .billing-history-row.is-ended .billing-history-main strong, .user-settings .billing-history-row.is-ended .billing-history-paid { color: var(--ink-muted); }
.user-settings .billing-manage { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-top: 20px; padding: 18px 20px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 10px; font-size: 13px; }
.user-settings .billing-manage strong { display: block; font-weight: 600; }
.user-settings .billing-manage p { margin: 3px 0 0; color: var(--ink-muted); font-size: 12px; line-height: 1.55; }
.user-settings .billing-manage .btn { flex-shrink: 0; }
.user-settings .billing-conference { margin: 28px 0 0; font-size: 13px; color: var(--ink-muted); }
.user-settings .billing-addons { margin-top: 40px; }
.user-settings .billing-subtitle { font-size: 13px; font-weight: 600; margin: 0; }
.user-settings .billing-addons-head { display: grid; grid-template-columns: minmax(0, 1fr) 90px 48px; gap: 16px; align-items: baseline; margin-bottom: 8px; }
.user-settings .billing-addons-head span { text-align: right; }
.user-settings .billing-addons-head span { font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-muted); }
.user-settings .billing-addon-rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border-color); }
.user-settings .billing-addon-rows li { display: grid; grid-template-columns: minmax(0, 1fr) 90px 48px; gap: 16px; align-items: center; padding: 11px 0; border-bottom: 1px solid var(--border-color); font-size: 13px; }
.user-settings .billing-addon-rows strong { font-weight: 600; margin-right: 8px; }
.user-settings .billing-addon-rows div span { color: var(--ink-muted); }
.user-settings .billing-addon-rows .billing-addon-price { text-align: right; font-weight: 500; }
.user-settings .billing-addon-rows .settings-text-button { justify-self: end; padding: 0; min-height: 0; font-size: 13px; font-weight: 600; }
body.dark-mode .settings-status.is-active { background: rgba(124, 199, 190, .16); color: #d2f1ed; box-shadow: inset 0 0 0 1px rgba(124, 199, 190, .45); }
body.dark-mode .user-settings .billing-offer.is-recommended { border-color: rgba(124, 199, 190, .6); box-shadow: none; }
body.dark-mode .user-settings .billing-summary .quota-track { background: rgba(255, 255, 255, .2); }
@media (max-width: 900px) {
.user-settings .billing-offers { grid-template-columns: 1fr; gap: 24px; }
.user-settings .billing-history-head { display: none; }
.user-settings .billing-history-row { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; }
.user-settings .billing-history-term:empty { display: none; }
.user-settings .billing-addon-rows li { grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; }
.user-settings .billing-addon-rows strong { display: block; }
.user-settings .billing-addon-rows .settings-text-button { min-height: 44px; padding: 0 4px; }
.user-settings .billing-offer-head { height: auto; flex-wrap: wrap; row-gap: 8px; }
.user-settings .billing-history-row { grid-template-columns: minmax(0, 1fr) auto; }
.user-settings .billing-history-term { grid-column: 1; grid-row: 2; }
.user-settings .billing-history-paid { grid-column: 2; grid-row: 2; text-align: right; color: var(--ink-muted); font-size: 12px; }
.user-settings .billing-buy { grid-template-columns: 1fr; gap: 20px; }
.user-settings .billing-buy-form { padding-left: 0; border-left: 0; padding-top: 20px; border-top: 1px solid var(--border-color); }
.user-settings .billing-history-main { grid-column: 1; grid-row: 1; }
.user-settings .billing-history-status { grid-column: 2; grid-row: 1; }
.user-settings .billing-history-term { font-size: 12px; color: var(--ink-muted); }
.user-settings .billing-buy-row, .user-settings .billing-manage { flex-direction: column; align-items: stretch; }
}
@media (max-width: 600px) {
.user-settings .billing-summary-head { flex-direction: column; align-items: stretch; }
.user-settings .billing-summary .quota-usage { grid-template-columns: 1fr; gap: 16px; }
.user-settings .billing-summary-actions .btn { width: 100%; }
.settings-nav { overflow-x: auto; gap: 20px; scrollbar-width: none; padding-right: 16px; }
.settings-nav button { white-space: nowrap; flex-shrink: 0; }
}
/* Anonymize page: choosing how to host a repository */
.hosting-options { border: 0; padding: 0; margin: 14px 0 0; min-width: 0; }
.hosting-options-title { font-size: 13px; font-weight: 600; margin: 0 0 8px; padding: 0; }
.hosting-option { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; gap: 10px; align-items: start; margin: 0 0 8px; padding: 12px 14px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 8px; cursor: pointer; font-weight: 400; }
.hosting-option:hover:not(.is-selected) { border-color: color-mix(in srgb, var(--accent) 45%, var(--border-color)); }
.hosting-option.is-selected { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--accent-soft); }
.hosting-option input { width: 16px; height: 16px; margin: 1px 0 0; accent-color: var(--accent); }
.hosting-subhead { font-size: 13px; font-weight: 600; margin: 22px 0 10px; padding-top: 18px; border-top: 1px solid var(--border-color); }
.hosting-option-body strong { display: block; font-size: 13px; font-weight: 600; line-height: 1.35; }
.hosting-option-body small { display: block; font-size: 12px; line-height: 1.45; color: var(--ink-muted); margin-top: 2px; }
.hosting-option-price { text-align: right; white-space: nowrap; line-height: 1.2; min-width: 64px; }
.hosting-limit { margin: 12px 0 0; padding: 10px 12px; border-radius: 8px; background: rgba(217, 119, 6, .08); border: 1px solid rgba(217, 119, 6, .28); font-size: 13px; font-weight: 500; }
body.dark-mode .hosting-limit { background: rgba(251, 191, 36, .1); border-color: rgba(251, 191, 36, .3); }
.hosting-cycle { margin-top: 8px; }
.hosting-option-price strong { display: block; font-size: 15px; font-weight: 600; }
.hosting-option-price small { display: block; font-size: 12px; color: var(--ink-muted); }
.hosting-payment > .hosting-limit:first-child { margin-top: 4px; }
.hosting-payment { margin-bottom: 18px; }
.inline-hosting .capacity-options summary, .hosting-payment .capacity-options summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; color: var(--accent); }
.hosting-payment .capacity-options summary::-webkit-details-marker { display: none; }
.hosting-payment .capacity-options summary::before { content: ""; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform .15s; }
.hosting-payment .capacity-options[open] summary::before { transform: rotate(45deg); }
.inline-hosting .hosting-terms { font-size: 12px; color: var(--ink-muted); margin: 4px 0 0; }
+6 -1
View File
@@ -103,6 +103,9 @@
"conference_not_found": "The requested conference could not be found.",
"conf_not_found": "The requested conference could not be found.",
"conf_not_activated": "The conference is not activated.",
"conference_payment_required": "This conference's review package is full or not paid. Ask the organizer, or host the repository yourself.",
"billing_plan_change_not_supported": "This conference plan cannot be changed.",
"billing_invalid_review_window": "Conference packages cover up to 180 days and must end in the future. Check the review dates.",
"billing_missing": "Billing information is required for this plan.",
"billing_name_missing": "A billing name is required.",
"billing_email_missing": "A billing email is required.",
@@ -169,7 +172,9 @@
"billing_webhook_failed": "The payment notification could not be processed.",
"billing_customer_not_found": "No billing account was found.",
"subscription_required_for_never_expiration": "Never-expiring repositories require the owner’s active monthly or annual author subscription. Choose a dated expiration or upgrade in Plans and billing.",
"invalid_billing_expiration": "Choose an expiration date within the next year, or select Never expire with an author subscription.",
"invalid_billing_expiration": "Choose an earlier expiration date. Free repositories can be scheduled 5 months ahead. Buy Extended expiration or Never expire for this repository in Plans and billing to keep it longer.",
"quota_exceeded": "Your free repositories are used up. Add this repository with a one-time payment, choose a subscription, or remove a repository you no longer need.",
"expiration_limit_reached": "This repository is already at its latest allowed expiration date. Buy Extended expiration or Never expire for it in Plans and billing to keep it longer.",
"pending_payment": "Awaiting payment"
},
"WARNINGS": {
+38 -27
View File
@@ -356,12 +356,42 @@
<section class="paper-settings-section hosting-settings" id="repository-hosting" v-show="detectedType" aria-labelledby="hosting-heading">
<h2 class="paper-section-eyebrow" id="hosting-heading">Hosting &amp; expiration</h2>
<p class="hosting-usage" v-if="detectedType === 'repo' &amp;&amp; billing?.enabled">
<div class="form-group conference-field">
<label class="paper-field-label" for="conference">Conference code <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<p class="conference-hosting" :class="{ 'is-covered': conferenceHosting() }" v-show="conference_data" role="status">
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
<template v-if="conferenceHosting()"> hosts this repository until {{ fmt?.date(conference_data?.endDate) }}, with automatic updates. You don't pay anything.</template>
<template v-else-if="billing?.enabled &amp;&amp; detectedType === 'repo'"> runs until {{ fmt?.date(conference_data?.endDate) }}. It doesn't pay for hosting, so this repository uses your own allowance.</template>
<template v-else> runs until {{ fmt?.date(conference_data?.endDate) }}.</template>
</p>
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
<small class="form-text text-muted" v-show="!conference_data">Submitting to a conference? Its code applies the venue's settings, and a paid conference hosts the repository for you.</small>
</div>
<p class="hosting-usage" v-if="detectedType === 'repo' &amp;&amp; billing?.enabled &amp;&amp; !conferenceHosting()">
<span v-if="isUpdate &amp;&amp; role !== 'owner'">Uses the owner's hosting</span>
<span v-else-if="hasAuthorHosting()">Your author subscription is active.</span>
<span v-else-if="conference">Conference hosting is checked on submission.</span>
<span v-else-if="hostingQuota?.repository">{{ hostingQuota.repository.used }} of {{ hostingQuota.repository.total }} active repositories · Free</span>
</p>
<div class="hosting-payment" v-if="detectedType === 'repo' &amp;&amp; billing?.enabled">
<template v-if="hostingNeedsPayment() &amp;&amp; !billingPending">
<p class="hosting-limit" role="status" v-if="options.expirationMode !== 'never' || hostingQuotaBlocked">{{ options.update &amp;&amp; !hostingQuotaBlocked &amp;&amp; hostingQuota?.repository?.used &lt; hostingQuota?.repository?.total ? 'Automatic updates need a paid option for this repository.' : 'Your free repositories are all in use.' }}</p>
<fieldset class="hosting-options" v-if="billing.checkoutEnabled !== false &amp;&amp; hostingOptionPlans().length">
<legend class="hosting-options-title">{{ hostingOneTime()?.id === 'repo_never' ? 'Keep this link online' : 'Choose how to host this repository' }}</legend>
<label class="hosting-option" v-for="plan in hostingOptionPlans()" :key="plan.id" :class="{ 'is-selected': hostingChoice() === plan.id || (plan.interval &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice())) }">
<input type="radio" name="hosting-plan" :value="plan.id" :checked="hostingChoice() === plan.id || (plan.interval &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice()))" @change="hostingPlan = plan.interval ? hostingSubscriptionPlan().id : plan.id">
<span class="hosting-option-body"><strong>{{ hostingOptionName(plan) }}</strong><small>{{ hostingOptionBlurb(plan) }}</small>
<span class="hosting-cycle billing-toggle" v-if="plan.interval &amp;&amp; authorPlans().length &gt; 1 &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice())" role="radiogroup" aria-label="Billing frequency"><button type="button" role="radio" v-for="cycle in authorPlans()" :key="cycle.id" :aria-checked="hostingSubscriptionPlan().id === cycle.id ? 'true' : 'false'" :class="{ 'is-on': hostingSubscriptionPlan().id === cycle.id }" @click.prevent="hostingCycle = cycle.id; hostingPlan = cycle.id">{{ cycle.interval === 'year' ? 'Yearly' : 'Monthly' }}<span class="billing-toggle-save" v-if="cycle.interval === 'year' &amp;&amp; hostingAnnualSavings()">−{{ hostingAnnualSavings() }}%</span></button></span></span>
<span class="hosting-option-price"><strong>{{ billingAmount(plan.interval ? hostingSubscriptionPlan() : plan) }}</strong><small>{{ plan.interval ? 'per ' + hostingSubscriptionPlan().interval : 'once' }}</small></span>
</label>
</fieldset>
<p v-else role="status">New purchases are temporarily unavailable. Your draft is kept.</p>
<p class="hosting-terms" v-if="authorPlans().some(p =&gt; p.id === hostingChoice())">Renews {{ hostingChoice() === 'author_annual' ? 'yearly' : 'monthly' }} until you cancel. Tax shown at checkout.</p>
<p class="hosting-terms" v-else>One-time payment, nothing renews. Tax shown at checkout.</p>
<details class="capacity-options" v-if="options.expirationMode !== 'never'"><summary>Keep using free hosting</summary><p>Remove a repository you no longer need, or enter the code of a paid conference above. Removing a repository affects its review link.</p><a href="/dashboard">Review my repositories</a><button type="button" class="task-text-button" @click="anonymizeRepo($event)" :disabled="sourceLoading || billingBusy">Recheck capacity and submit</button></details>
</template>
</div>
<h4 class="hosting-subhead" v-if="hostingNeedsPayment() &amp;&amp; !billingPending">Expiration</h4>
<fieldset class="expiration-choices">
<legend class="sr-only">Expiration</legend>
<label class="expiration-choice"><input type="radio" name="expiration-choice" :checked="options.expirationMode !== 'never'" @change="useDatedHosting()" :disabled="billingPending"><span>Expire on a date</span></label>
@@ -375,37 +405,17 @@
</div>
<div class="hosting-property"><label for="expiration">Then</label><select id="expiration" class="form-control" v-model="options.expirationMode"><option value="remove">Remove content</option><option value="redirect">Redirect to GitHub</option></select></div>
</div>
<label class="expiration-choice" v-if="detectedType === 'repo'"><input type="radio" name="expiration-choice" :checked="options.expirationMode === 'never'" @change="options.expirationMode = 'never'" :disabled="billingPending"><span>Never expire<small v-if="options.expirationMode !== 'never' || hasAuthorHosting()">{{ hasAuthorHosting() ? 'Included in your subscription' : 'Author subscription' }}</small></span></label>
<label class="expiration-choice" v-if="detectedType === 'repo'"><input type="radio" name="expiration-choice" :checked="options.expirationMode === 'never'" @change="options.expirationMode = 'never'" :disabled="billingPending"><span>Never expire<small v-if="(options.expirationMode !== 'never' || hasAuthorHosting() || repositoryGrants('repo_never').length) &amp;&amp; !(hostingNeedsPayment() &amp;&amp; hostingOptionPlans().length)">{{ neverExpireTag() }}</small></span></label>
</fieldset>
<p class="form-text text-muted" v-if="options.expirationMode === 'redirect'">Visitors will be redirected to the original GitHub source on this date, revealing its identity.</p>
<div class="form-group conference-field">
<label class="paper-field-label" for="conference">Conference code <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<small class="form-text text-muted" v-show="conference_data">
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a> expires {{ fmt?.date(conference_data?.endDate) }}.
</small>
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
<small class="form-text text-muted" v-show="!conference_data">Link to a conference to apply its shared defaults.</small>
</div>
<div v-if="detectedType === 'repo' &amp;&amp; billing?.enabled" class="inline-hosting">
<p v-if="taskValidationError" role="alert" class="text-danger">{{ taskValidationError }}</p>
<p v-if="billingError" role="alert" class="text-danger">{{ billingError }} <button type="button" class="task-text-button" @click="loadBilling()" :disabled="billingBusy">Reload billing</button></p>
<div v-if="billingPending" role="status"><p><strong>Confirming your payment…</strong></p><p class="form-text text-muted">Your draft is saved. Please do not pay again.</p></div>
<p v-else-if="billingNotice" role="status" class="form-text text-muted">{{ billingNotice }}</p>
<template v-if="hostingNeedsPayment() &amp;&amp; !billingPending">
<p v-if="options.expirationMode !== 'never'" class="capacity-reason">{{ options.update &amp;&amp; !hostingQuotaBlocked &amp;&amp; hostingQuota?.repository?.used < hostingQuota?.repository?.total ? 'Automatic updates require Author hosting.' : 'Your free hosting allowance is full.' }}</p>
<div class="subscription-choice" v-if="authorPlans().length &amp;&amp; billing.checkoutEnabled !== false">
<h3>Author subscription</h3>
<fieldset class="billing-cycle"><legend class="sr-only">Billing frequency</legend><label class="cycle-option" v-for="plan in authorPlans()" :key="plan.id" :class="{'selected': selectedHostingPlan()?.id === plan.id}"><input type="radio" name="hosting-plan" :value="plan.id" :checked="selectedHostingPlan()?.id === plan.id" @change="hostingPlan = plan.id"><span>{{ billingPrice(plan) }}</span></label></fieldset>
<p>{{ selectedHostingPlan()?.repositories }} repositories, {{ billingBytes(selectedHostingPlan()?.storage) }}. Coauthors included.</p>
<p class="hosting-terms">Renews {{ selectedHostingPlan()?.interval === 'year' ? 'annually' : 'monthly' }} until cancelled. Tax shown at checkout.</p>
<p class="hosting-terms">If paid access ends, never-expiring repositories expire after 90 days.</p>
</div>
<p v-else role="status">New purchases are temporarily unavailable. Your draft is kept.</p>
<details class="capacity-options" v-if="options.expirationMode !== 'never'"><summary>Keep using free hosting</summary><p>Remove a repository you no longer need, or use conference hosting. Removing a repository affects its review link.</p><a href="/dashboard">Review my repositories</a><button type="button" class="task-text-button" @click="anonymizeRepo($event)" :disabled="sourceLoading || billingBusy">Recheck capacity and submit</button></details>
</template>
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' &amp;&amp; !billingPending &amp;&amp; !hasAuthorHosting() &amp;&amp; repositoryGrants('repo_never').length">No scheduled expiration. Paid once for this repository.</p>
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' &amp;&amp; !billingPending">No scheduled expiration while subscribed. A 90-day grace period applies if paid access ends.</p>
<details class="repository-pass" v-if="isUpdate &amp;&amp; role === 'owner' &amp;&amp; !hasAuthorHosting()"><summary>Hosting just this repository?</summary><div v-for="plan in (billing?.plans || []).filter(p =&gt; p.id === 'author_pass')" :key="plan.id"><p>{{ billingPrice(plan) }} · {{ billingBytes(plan.storage) }} for 180 days. No renewal. Does not include Never expire.</p><button type="button" class="btn btn-outline-ink" :disabled="billingBusy || hostingSaving || sourceLoading || billing.checkoutEnabled === false || billingPending" @click="checkoutRepository(true)">Review repository pass</button></div></details>
<details class="repository-pass" v-if="isUpdate &amp;&amp; role === 'owner' &amp;&amp; !hasAuthorHosting() &amp;&amp; !hostingNeedsPayment() &amp;&amp; repositoryAddonPlans().length"><summary>Pay once for just this repository</summary><div v-for="plan in repositoryAddonPlans()" :key="plan.id"><p><strong>{{ plan.name }}</strong> · {{ billingPrice(plan) }}. {{ plan.description }}</p><button type="button" class="btn btn-outline-ink" :disabled="billingBusy || hostingSaving || sourceLoading || billing.checkoutEnabled === false || billingPending" @click="checkoutRepository(plan.id)">Buy {{ plan.name.toLowerCase() }}</button></div></details>
</div>
</section>
@@ -452,8 +462,8 @@
<div class="anonymize-submit-bar" v-show="detectedType">
<template v-if="detectedType === 'repo'">
<button type="button" class="btn btn-ink" v-if="billingPending" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button>
<button type="submit" class="btn btn-ink" v-else :disabled="sourceLoading || billingBusy || hostingSaving || billingLoading || (hostingNeedsPayment() &amp;&amp; (!authorPlans().length || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () =&gt; { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
<p class="checkout-return-note" v-if="hostingNeedsPayment() &amp;&amp; !billingPending">Review payment in Stripe, then return to finish.</p>
<button type="submit" class="btn btn-ink" v-else :disabled="sourceLoading || billingBusy || hostingSaving || billingLoading || (hostingNeedsPayment() &amp;&amp; ((!authorPlans().length &amp;&amp; !hostingOneTime()) || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () =&gt; { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
</template>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Pull Request
@@ -467,6 +477,7 @@
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Gist
</button>
<p class="checkout-return-note" v-if="hostingNeedsPayment() &amp;&amp; !billingPending">You pay in Stripe, then come back here to finish.</p>
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved ·" : "Unsaved changes ·" }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved" : "Discard" }}</button></div>
</div>
+4 -4
View File
@@ -22,7 +22,7 @@
<span>ID <b class="commit-hash" v-text="conference?.conferenceID"></b></span>
<span>Review window <b>{{ fmt?.date(conference?.startDate, 'mediumDate') }} &ndash; {{ fmt?.date(conference?.endDate, 'mediumDate') }}</b></span>
<span>Repositories <b>{{ fmt?.number(conference?.repositories?.length) }}</b></span>
<span>Plan <b v-text="conference?.plan?.name || conference?.plan?.planID || &#x27;Free&#x27;"></b></span>
<span>Plan <b v-text="conferencePlanName(conference?.plan?.planID)"></b></span>
<span v-if="conference?.price">Cost so far <b>{{ fmt?.number(conference?.price, 2) }} &euro;</b></span>
</div>
@@ -36,9 +36,9 @@
<p>This conference is waiting for payment. Authors can join after payment is confirmed. Checkout returns to this conference. Authors within the package do not pay separately.</p>
<p v-if="billingLoading" role="status">Loading payment options…</p>
<p v-if="billingError" role="alert">{{ billingError }}</p>
<div v-for="plan in (billing?.plans || []).filter(p =&gt; p.id === 'conference')" :key="plan.id">
<p>{{ billingPrice(plan) }} for up to {{ plan.repositories }} repositories and {{ billingBytes(plan.storage) }} of source files during the review window. Taxes, if applicable, appear at checkout.</p>
<button type="button" class="btn btn-primary" @click="checkout('conference', conference.conferenceID)" :disabled="billingBusy || billingPending || billing?.checkoutEnabled === false">{{ billingBusy ? 'Please wait…' : 'Pay for review package' }}</button>
<div v-for="plan in (billing?.plans || []).filter(p =&gt; p.id === conference?.plan?.planID)" :key="plan.id">
<p><strong>{{ plan.name }} package</strong>: {{ billingPrice(plan) }} for up to {{ plan.repositories }} repositories and {{ billingBytes(plan.storage) }} of source files during the review window. Taxes, if applicable, appear at checkout.</p>
<button type="button" class="btn btn-primary" @click="checkout(plan.id, conference.conferenceID)" :disabled="billingBusy || billingPending || billing?.checkoutEnabled === false">{{ billingBusy ? 'Please wait…' : 'Pay for review package' }}</button>
</div>
<p><a href="/profile#settings-billing">Manage invoices</a></p>
</section>
+48 -8
View File
@@ -122,6 +122,25 @@
<!-- Plans and billing -->
<h4 class="faq-cat" id="faq-plans-title">Plans and billing</h4>
<div class="panel-group" id="faq-plans" role="tablist" aria-label="Plans and billing" aria-multiselectable="true">
<div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingWhyPaid">
<h3 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-plans" href="#whyPaid" aria-expanded="false" aria-controls="whyPaid">Why is Anonymous GitHub no longer completely free?</a>
</h3>
</div>
<div id="whyPaid" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingWhyPaid">
<div class="panel-body p-3">
<p>Anonymous GitHub started as a small free service and was paid for out of pocket. Three things have changed since then:</p>
<ul>
<li><strong>Servers cost much more.</strong> Every hosted repository needs storage, bandwidth and backups, and these costs have risen sharply.</li>
<li><strong>Many more people use it.</strong> More users means more repositories to store and keep up to date, more support requests and more maintenance.</li>
<li><strong>AI has made security work heavier.</strong> AI-driven scrapers and automated abuse send far more traffic than before. Blocking them and keeping hosted content safe takes constant work.</li>
</ul>
<p>The core service stays free: anonymizing a repository, sharing the link and reviewer access cost nothing, and reviewers never need an account. Most authors never need to pay. The paid options are small one-time payments for one repository, plus an optional subscription for heavy users. They are priced to cover running costs, not to make a profit.</p>
<p>The source code remains open, so you can still <a href="#faq-hosting">host your own instance</a> for free.</p>
</div>
</div>
</div>
<div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingFreePlan">
<h3 class="panel-title">
@@ -130,7 +149,7 @@
</div>
<div id="freePlan" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingFreePlan">
<div class="panel-body p-3">
<p>The default free allowance is 5 repositories you own and 2 GiB of source files in total. Core anonymization and reviewer access remain free. You can manually refresh a repository within your allowance and choose a dated expiration up to one year ahead.</p>
<p>The default free allowance is 5 repositories you own and 2 GiB of source files in total. Core anonymization and reviewer access remain free. You can manually refresh a repository within your allowance and choose a dated expiration up to 5 months ahead, then extend it again before it expires. Repositories submitted to a conference can stay online until the end of its review window.</p>
<p>Repositories shared with you as a coauthor do not use your own slots. Gists and pull requests are not included in these repository quotas. The usage shown in your account is the allowance available on this instance.</p>
</div>
</div>
@@ -157,8 +176,15 @@
</div>
<div id="reviewPass" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingReviewPass">
<div class="panel-body p-3">
<p>The author review pass covers one selected repository for 180 days, with a separate 2 GiB allowance and automatic refresh when enabled. It is a one-time purchase and does not renew automatically.</p>
<p>Enter the anonymized ID of an existing repository you own in <a href="/profile#settings-billing">Plans and billing</a>. A pass does not unlock Never expire and does not override the repository's chosen expiration date.</p>
<p>Yes. These one-time add-ons apply to a single repository you own. None of them renews automatically:</p>
<ul>
<li><strong>Paper pack</strong>: an extra repository, automatic updates for 180 days and one more year before it expires, in a single payment. It covers what one paper usually needs during review.</li>
<li><strong>Extra repository</strong>: host one more repository once your free repositories are used up. It has its own 2 GiB allowance and does not count toward your free repositories. It lasts as long as the repository exists.</li>
<li><strong>Live updates</strong>: automatic refresh for 180 days, with a separate 2 GiB allowance.</li>
<li><strong>Extended expiration</strong>: choose an expiration date up to one year after purchase. Buying it again adds another year.</li>
<li><strong>Never expire</strong>: the repository has no expiration date. A full refund or a payment dispute ends it after a 90-day grace period.</li>
</ul>
<p>When your free allowance is full, the Extra repository and Paper pack options are offered as you create the repository. You can also buy an add-on from the repository's edit page, or enter the anonymized ID of an existing repository in <a href="/profile#settings-billing">Plans and billing</a>. Current prices are shown there.</p>
</div>
</div>
</div>
@@ -170,9 +196,9 @@
</div>
<div id="conferencePlans" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingConferencePlans">
<div class="panel-body p-3">
<p>A conference review package provides a shared allowance of 100 repositories and 50 GiB for a review window of up to 180 days. The organizer pays once; the package does not renew automatically. A lab organizing a review can use this shared package too.</p>
<p>The paid conference must be activated by payment before authors can submit under its Conference ID. Eligible repositories use the conference allowance before an author's personal allowance, so authors do not each need a subscription for that capacity.</p>
<p>The package has a fixed review window. Never expire still requires the repository owner's monthly or annual author subscription. See the available offer when creating a conference.</p>
<p>A conference review package provides a shared allowance for a review window of up to 180 days, with automatic updates for every submission. It comes in three sizes: Workshop (25 repositories, 10 GiB), Conference (100 repositories, 50 GiB) and Large conference (400 repositories, 200 GiB). Choose the size when you create the conference; it cannot change afterwards. The organizer pays once and authors pay nothing (the price is shown when you create the conference); the package does not renew automatically. A lab organizing a review can use this shared package too.</p>
<p>New conferences no longer have a free plan: every new conference is a review package. Conferences created earlier keep working as before. The conference must be activated by payment before authors can submit under its Conference ID. Eligible repositories use the conference allowance before an author's personal allowance, so authors do not each need a subscription for that capacity.</p>
<p>The package has a fixed review window. Never expire still requires the repository owner's author subscription or a Never expire add-on for that repository.</p>
</div>
</div>
</div>
@@ -198,7 +224,7 @@
<div id="capacity" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingCapacity">
<div class="panel-body p-3">
<p>The repository limit counts work you own. Storage counts the source-file sizes reserved for those repositories, not just files a reviewer opens. Archived or failed repositories can still reserve capacity, and a refresh does not automatically reduce a previous storage reservation.</p>
<p>At the repository limit you cannot add another repository to that allowance. An edit or refresh must also fit the available storage. Remove work you no longer need, choose a paid plan, or use an eligible conference allowance.</p>
<p>At the repository limit you cannot add another repository to that allowance. An edit or refresh must also fit the available storage. Remove work you no longer need, add an Extra repository for the new one, choose a subscription, or use an eligible conference allowance.</p>
<p>Removing a repository releases its reservation when capacity is next checked. Expired repositories may continue to reserve capacity until cleanup. Reaching a limit does not itself delete existing review links.</p>
</div>
</div>
@@ -211,7 +237,7 @@
</div>
<div id="neverExpire" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingNeverExpire">
<div class="panel-body p-3">
<p>Never expire is available to monthly and annual author subscribers. If paid access ends, these repositories remain available for a <strong>90-day grace period</strong>, then expire and their hosted content is removed.</p>
<p>Never expire is available to monthly and annual author subscribers, or for one repository with the one-time Never expire add-on. If a subscription ends, these repositories remain available for a <strong>90-day grace period</strong>, then expire and their hosted content is removed.</p>
<p>Renewing during the grace period keeps them active. After expiration, you must renew and explicitly reactivate the repository; restoring content also requires access to its GitHub source. A repository does not become public on GitHub when it expires.</p>
<p>Existing never-expiring repositories created before this billing policy retain their settings. Never expire is not a guarantee of perpetual service availability.</p>
</div>
@@ -231,6 +257,20 @@
</div>
</div>
</div>
<div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingRefunds">
<h3 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-plans" href="#refunds" aria-expanded="false" aria-controls="refunds">Can I get a refund?</a>
</h3>
</div>
<div id="refunds" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingRefunds">
<div class="panel-body p-3">
<p>Refunds are handled case by case. <a href="mailto:tdurieux@anonymous.4open.science">Email the maintainer</a> with the date of the payment and the repository it was for.</p>
<p>A full refund ends what that payment bought. For a Paper pack, this means all three parts: the extra repository, automatic updates and the extended expiration. A Never expire repository then expires after a 90-day grace period. A partial refund keeps your access.</p>
<p>To stop a subscription, cancel its renewal instead of asking for a refund. You keep access until the end of the period you paid for.</p>
</div>
</div>
</div>
<div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingPaymentHandling">
<h3 class="panel-title">
+32 -11
View File
@@ -2,8 +2,8 @@
<div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{ editionMode ? 'Edit' : 'New' }}</span></div>
<h1 class="paper-page-title">{{ editionMode ? 'Edit' : 'Create a' }} <em>conference</em></h1>
<p class="paper-page-lede">
Give chairs access to every anonymization submitted to a venue, and lift
author quotas during the review window.
Give chairs access to every anonymization submitted to a venue. One
payment hosts every submission, so authors don't pay during the review window.
</p>
<div class="paper-settings-body">
@@ -130,19 +130,40 @@
<section id="conf-plan" class="paper-settings-section">
<div class="paper-section-eyebrow">Plan</div>
<p class="paper-section-copy">The review package is prepaid and covers up to 180 days. Pay from the conference page after creating it. Plan and review dates cannot change after creation.</p>
<div class="paper-plan-grid">
<label class="paper-plan-card" v-for="(plan, index) in plans" :class="{&#x27;selected&#x27;: options?.plan?.planID == plan?.id}">
<input type="radio" :disabled="editionMode" name="planPicker" v-field="{ state: viewState, set: value =&gt; { options.plan.planID = value }, value: options?.plan?.planID, form: &quot;conference&quot;, options: {} }" :value="plan?.id">
<p class="paper-section-copy" v-if="editionMode">{{ conferencePlanName(options?.plan?.planID) }}. The plan and review dates can't change after creation.</p>
<template v-else-if="conferenceTiers().length">
<p class="paper-section-copy">Pick the package that fits the number of submissions you expect. One payment hosts every submission, so authors don't pay.</p>
<div class="conference-tiers" role="radiogroup" aria-label="Review package size">
<label class="conference-tier" v-for="tier in conferenceTiers()" :key="tier.id" :class="{ 'is-selected': options?.plan?.planID === tier.id }">
<input type="radio" name="planPicker" :value="tier.id" :checked="options?.plan?.planID === tier.id" @change="options.plan.planID = tier.id">
<span class="conference-tier-head"><strong>{{ tier.name }}</strong><span class="conference-tier-pill" v-if="tier.id === 'conference' &amp;&amp; conferenceTiers().length &gt; 1">Most common</span></span>
<span class="conference-tier-who">{{ conferenceAudience(tier.id) }}</span>
<span class="conference-tier-price" v-if="tier.amount != null"><strong>{{ conferenceAmount(tier) }}</strong> once</span>
<span class="conference-tier-size">Up to {{ tier.repositories }} papers · {{ conferenceBytes(tier.storage) }}</span>
<span class="conference-tier-per" v-if="tier.amount != null &amp;&amp; tier.repositories">About {{ conferenceAmount(tier, tier.amount / tier.repositories) }} per paper</span>
</label>
</div>
<div class="conference-package">
<h3>Every package includes</h3>
<ul class="billing-offer-list">
<li>A review window of up to 180 days</li>
<li>Automatic updates for every submission</li>
<li>Links stay online until the review window ends</li>
<li>A dashboard of every submission for chairs</li>
</ul>
<p class="conference-package-note">Nothing renews · Tax shown at checkout · Expecting more submissions? Email <a href="mailto:tdurieux@anonymous.4open.science">tdurieux@anonymous.4open.science</a></p>
</div>
<p class="paper-section-copy">Create the conference, then pay from its page. Authors can submit once the payment is confirmed.</p>
</template>
<div class="paper-plan-grid" v-else-if="plans.length">
<label class="paper-plan-card" v-for="plan in plans" :key="plan.id" :class="{ selected: options?.plan?.planID == plan?.id }">
<input type="radio" name="planPicker" v-field="{ state: viewState, set: value =&gt; { options.plan.planID = value }, value: options?.plan?.planID, form: &quot;conference&quot;, options: {} }" :value="plan?.id">
<div class="paper-plan-head" v-text="plan?.name"></div>
<div class="paper-plan-price">
{{ plan.id === "free_conference" ? "Free" : "Prepaid package" }}
<span class="paper-plan-per">{{ plan.id === "free_conference" ? "" : "Price on the conference page" }}</span>
</div>
<div class="paper-plan-price">{{ plan.pricePerRepo &gt; 0 ? fmt?.number(plan.pricePerRepo, 2) + ' € per repository' : 'Free' }}</div>
<div class="paper-plan-desc" v-html="sanitize(plan?.description)"></div>
</label>
</div>
<p class="settings-notice" v-else-if="plansLoaded" role="status">New conferences are unavailable right now. Please try again later.</p>
</section>
<section id="conf-billing" class="paper-settings-section" v-if="plan?.pricePerRepo &gt; 0">
+97 -29
View File
@@ -32,38 +32,106 @@ jane.smith@example.org" v-field="{ state: viewState, set: value =&gt; { terms =
<div v-if="billingError" class="settings-notice" role="alert">{{ billingError }} <button type="button" class="btn btn-outline-ink" @click="loadBilling()" :disabled="billingBusy">Try again</button></div>
<p v-if="billingNotice" class="settings-notice" role="status">{{ billingNotice }}</p>
<div v-if="billingPending" class="settings-notice" role="status"><strong>Confirming your payment</strong><p>Your allowance will update after confirmation. Please do not pay again.</p><button type="button" class="btn btn-outline-ink" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button></div>
<section class="settings-row"><h2>Current plan</h2><div>
<div class="settings-inline"><div><strong>{{ profileAuthorGrant() ? 'Author subscription' : (billing?.enabled ? 'Free' : 'Account allowance') }}</strong><p class="settings-help" v-if="profileAuthorGrant()">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Renewal cancelled · Access until ' : 'Paid access through ' }}{{ billingDate(profileAuthorGrant().until) }}</p></div>
<button type="button" class="btn btn-outline-ink" v-if="billing?.enabled && !profileAuthorGrant() && !billingPending && !showAuthorPlans && profileAuthorPlans().length" @click="openProfileUpgrade()">View Author plan</button>
<section class="billing-summary">
<div class="billing-summary-head">
<div>
<p class="billing-eyebrow">Current plan</p>
<h2 class="billing-plan-name">{{ profileAuthorGrant() ? profileGrantName(profileAuthorGrant()) : (billing?.enabled ? 'Free' : 'Account allowance') }}</h2>
<p class="billing-plan-meta" v-if="profileAuthorGrant()">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Renewal cancelled · access until ' : 'Renews ' }}{{ billingDate(profileAuthorGrant().until) }}<template v-if="profileAuthorPurchase()?.amount != null"> · {{ billingPrice(profileAuthorPurchase()) }}</template></p>
<p class="billing-plan-meta" v-else-if="billing?.allowance">{{ billing.allowance.repositories }} repositories · {{ billingBytes(billing.allowance.storage) }} · links up to {{ billing.freeExpirationMonths || 5 }} months</p>
</div>
<div class="billing-summary-actions" v-if="profileAuthorGrant() &amp;&amp; billing?.portal">
<button type="button" class="btn btn-outline-ink" @click="billingPortal()" :disabled="billingBusy">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Resume renewal' : 'Change or cancel plan' }}</button>
</div>
</div>
<p class="settings-help" v-if="billing?.allowance">{{ billing.allowance.repositories }} repositories · {{ billingBytes(billing.allowance.storage) }}</p>
<div id="settings-quota"><quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage><p v-else role="status">Loading usage…</p></div>
<p class="settings-help">Coauthored repositories use their owner's allowance. Conference hosting is separate.</p>
<p class="settings-notice" v-if="quota?.repository && !quota.repository.unlimited && quota.repository.used >= quota.repository.total">Your repository allowance is full. <a href="/dashboard">Review your repositories</a> to remove work you no longer need.</p>
<p class="settings-notice" v-if="profilePastSubscription()">Your previous subscription is no longer active. Never-expiring repositories receive a 90-day grace period after paid access ends. <a href="/dashboard">Review repository expiration dates</a>.</p>
<p class="settings-notice" v-if="profileAuthorGrant()?.status === 'past_due'">A subscription payment is overdue. Use Manage billing to update your payment details.</p>
<p class="settings-help" v-if="profileAuthorGrant()?.cancelAtPeriodEnd">When paid access ends, never-expiring repositories enter a 90-day grace period. Dated repositories keep their expiration dates.</p>
<div class="settings-plan billing-plan" v-if="showAuthorPlans && !profileAuthorGrant() && !billingPending && profileAuthorPlans().length">
<h3 id="settings-plan-comparison" tabindex="-1">Compare plans</h3>
<table class="settings-comparison"><caption class="sr-only">Free and Author hosting</caption><thead><tr><th scope="col">Included</th><th scope="col">Free</th><th scope="col">Author</th></tr></thead><tbody>
<tr><th scope="row">Active repositories</th><td>{{ billing.allowance?.repositories }}</td><td>{{ profileSelectedPlan()?.repositories }}</td></tr>
<tr><th scope="row">Source storage</th><td>{{ billingBytes(billing.allowance?.storage) }}</td><td>{{ billingBytes(profileSelectedPlan()?.storage) }}</td></tr>
<tr><th scope="row">Expiration</th><td>Scheduled</td><td>Scheduled or never</td></tr>
<tr><th scope="row">Coauthors</th><td>Included</td><td>Included</td></tr>
<tr><th scope="row">Reviewer access</th><td>Free</td><td>Free</td></tr>
</tbody></table>
<fieldset class="settings-cycles"><legend class="sr-only">Billing frequency</legend><label v-for="plan in profileAuthorPlans()" :class="{selected: profileSelectedPlan()?.id === plan.id}"><input type="radio" name="profile-plan" :checked="profileSelectedPlan()?.id === plan.id" @change="profilePlan = plan.id">{{ billingPrice(plan) }}</label></fieldset>
<p class="settings-help">Renews {{ profileSelectedPlan()?.interval === 'year' ? 'annually' : 'monthly' }} until cancelled. Tax shown at checkout.</p><p class="settings-help">If paid access ends, never-expiring repositories expire after a 90-day grace period.</p>
<div class="settings-actions"><button type="button" class="btn btn-ink" :disabled="billingBusy || billing.checkoutEnabled === false" @click="checkout(profileSelectedPlan().id)">{{ billingBusy ? 'Please wait…' : 'Continue to checkout' }}</button><button type="button" class="btn btn-outline-ink" @click="showAuthorPlans = false">Keep current plan</button></div>
<p class="settings-notice" v-if="quota?.repository &amp;&amp; !quota.repository.unlimited &amp;&amp; quota.repository.used >= quota.repository.total">Your free repositories are used up. Add the next one for {{ profileSlotPlan() ? billingPrice(profileSlotPlan()) : 'a one-time payment' }} when you create it, or <a href="/dashboard">remove work you no longer need</a>.</p>
<p class="settings-notice" v-if="profileAuthorGrant()?.status === 'past_due'">A subscription payment is overdue. Update your payment details in Manage billing and invoices.</p>
<p class="settings-notice" v-if="profilePastSubscription()">Your previous subscription has ended. Never-expiring repositories stay online for a 90-day grace period. <a href="/dashboard">Review expiration dates</a>.</p>
<p class="billing-footnote">Coauthored, paid and conference repositories aren't counted here.</p>
</section>
<section class="billing-section" v-if="billing?.enabled &amp;&amp; !profileAuthorGrant() &amp;&amp; (profilePackPlan() || profileAuthorPlans().length)" aria-labelledby="settings-plan-comparison">
<h2 class="billing-section-title" id="settings-plan-comparison" tabindex="-1">Get more</h2>
<div class="billing-offers" v-if="!(showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending)">
<article class="billing-offer is-recommended" v-if="profilePackPlan()">
<span class="billing-offer-pill">Recommended</span>
<div class="billing-offer-head"><p class="billing-eyebrow">For one paper</p></div>
<h3>Paper pack</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profilePackPlan()) }}</strong> once</p>
<ul class="billing-offer-list"><li>One more repository, outside your free ones</li><li>Its own {{ billingBytes(profileSlotPlan()?.storage || profilePassPlan()?.storage) }} of storage</li><li>Automatic updates for 180 days</li><li>Online for up to a year</li><li>No subscription</li></ul>
<button type="button" class="btn btn-ink" @click="openProfileAddon('repo_pack')" :disabled="billingPending || billing.checkoutEnabled === false">Buy a Paper pack</button>
</article>
<article class="billing-offer" v-if="profileAuthorPlans().length">
<div class="billing-offer-head"><p class="billing-eyebrow">For many papers</p><div class="billing-toggle" role="radiogroup" aria-label="Billing frequency"><button type="button" role="radio" v-for="plan in profileAuthorPlansByValue()" :key="plan.id" :aria-checked="profileSelectedPlan()?.id === plan.id ? 'true' : 'false'" :class="{ 'is-on': profileSelectedPlan()?.id === plan.id }" @click="profilePlan = plan.id">{{ plan.interval === 'year' ? 'Yearly' : 'Monthly' }}<span v-if="plan.interval === 'year' &amp;&amp; profileAnnualSavings()" class="billing-toggle-save">−{{ profileAnnualSavings() }}%</span></button></div></div>
<h3>Author subscription</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profileSelectedPlan()) }}</strong> per {{ profileSelectedPlan()?.interval === 'year' ? 'year' : 'month' }}</p>
<ul class="billing-offer-list"><li>{{ profileSelectedPlan()?.repositories }} repositories, {{ billingBytes(profileSelectedPlan()?.storage) }}</li><li>Automatic updates</li><li>Links that never expire</li><li>Coauthors included</li><li>Cancel renewal anytime</li></ul>
<button type="button" class="btn btn-outline-ink" :disabled="billingBusy || billing.checkoutEnabled === false" @click="checkout(profileSelectedPlan().id)">{{ billingBusy ? 'Please wait…' : 'Subscribe' }}</button>
</article>
</div>
<p class="settings-help" v-if="billing && (!billing.enabled || billing.checkoutEnabled === false)">New purchases are not available. Existing billing can still be managed below.</p>
</div></section>
<section class="settings-row"><h2>Billing</h2><div><div class="settings-inline" v-if="billing?.portal"><p class="settings-help">Invoices, payment details, and renewal.</p><button type="button" class="btn btn-outline-ink" @click="billingPortal()" :disabled="billingBusy">Manage billing</button></div><p class="settings-help" v-else>No billing account available.</p><button type="button" class="settings-text-button" v-if="billing?.enabled && !billingPending" @click="refreshBilling()" :disabled="billingBusy">Refresh payment status</button></div></section>
<section class="settings-row" v-if="billing?.enabled"><h2>Repository passes</h2><div><div class="settings-inline"><p class="settings-help">One repository for 180 days.<br>No subscription or automatic renewal.</p><button type="button" class="btn btn-outline-ink" v-if="profilePassPlan()" @click="showRepositoryPass = !showRepositoryPass" :disabled="billingPending">Review a pass</button></div>
<div class="settings-pass" v-if="showRepositoryPass && profilePassPlan() && !billingPending"><label for="billing-repository">Repository ID you own</label><input id="billing-repository" class="form-control" v-model="billingRepository" placeholder="Existing anonymized repository ID" autocomplete="off"><p class="settings-help">{{ billingPrice(profilePassPlan()) }} · {{ billingBytes(profilePassPlan().storage) }} · Never expire is not included.</p><button type="button" class="btn btn-ink" :disabled="billingBusy || !billingRepository.trim() || billing.checkoutEnabled === false" @click="checkout('author_pass', billingRepository.trim())">Continue to checkout</button></div>
</div></section>
<section class="settings-row" v-if="billing?.grants?.length"><h2>Your purchases</h2><div><div class="settings-purchase" v-for="grant in billing.grants" :key="grant.scope + grant.planId + grant.until"><strong>{{ profileGrantName(grant) }}</strong><span>{{ grant.scope }}</span><p class="settings-help">{{ grant.status }}<template v-if="grant.status !== 'revoked'"> · Paid access through {{ billingDate(grant.until) }}{{ grant.cancelAtPeriodEnd ? ' · Renewal cancelled' : '' }}</template></p></div></div></section>
<section class="settings-row"><h2>Conference hosting</h2><div class="settings-inline"><p class="settings-help">Shared hosting is managed per conference.</p><a href="/conferences" class="btn btn-outline-ink">My conferences</a></div></section>
<div class="billing-addons" v-if="profileSingleAddons().length &amp;&amp; !(showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending)">
<div class="billing-addons-head"><h3 class="billing-subtitle">Or pay for one thing</h3><span>One-time</span></div>
<ul class="billing-addon-rows"><li v-for="plan in profileSingleAddons()" :key="plan.id"><div><strong>{{ plan.name }}</strong><span>{{ profileAddonBlurb(plan.id) }}</span></div><span class="billing-addon-price">{{ billingAmount(plan) }}</span><button type="button" class="settings-text-button" @click="openProfileAddon(plan.id)" :disabled="billingPending || billing.checkoutEnabled === false">Buy</button></li></ul>
</div>
<div class="billing-buy" v-if="showRepositoryPass &amp;&amp; profileAddon() &amp;&amp; !billingPending" id="settings-addons-title" tabindex="-1">
<div class="billing-buy-info">
<button type="button" class="btn btn-outline-ink billing-back" @click="showRepositoryPass = false">← All options</button>
<h3>{{ profileAddon().name }}</h3>
<p class="billing-offer-price"><strong>{{ billingAmount(profileAddon()) }}</strong> once</p>
<ul class="billing-offer-list"><li v-for="item in profileAddonFeatures(profileAddon().id)" :key="item">{{ item }}</li></ul>
</div>
<div class="billing-buy-form">
<template v-if="profileRepositories().length">
<label class="paper-field-label" for="billing-repository">Which repository?</label>
<select id="billing-repository" class="form-control" v-model="billingRepository"><option value="" disabled>Choose a repository</option><option v-for="repo in profileRepositories()" :key="repo.repoId" :value="repo.repoId">{{ repo.repoId }} · {{ profileRepositoryExpiry(repo) }}</option></select>
<button type="button" class="btn btn-ink" :disabled="billingBusy || !billingRepository || billing.checkoutEnabled === false" @click="checkout(profileAddon().id, billingRepository)">{{ billingBusy ? 'Please wait…' : 'Continue to checkout' }}</button>
<p class="billing-footnote">One-time payment. Tax shown at checkout.</p>
</template>
<p class="billing-buy-desc" v-else>You have no repositories yet. <a href="/anonymize">Create one</a> first.</p>
<p class="billing-footnote" v-if="['repo_slot', 'repo_pack'].includes(profileAddon().id)">Adding a new repository? <a href="/anonymize">Start creating it</a>; the form offers this once your free repositories are in use.</p>
</div>
</div>
<details class="billing-compare"><summary>Compare all features</summary><div class="billing-compare-card">
<table class="settings-comparison"><caption class="sr-only">Free, Paper pack and Author hosting</caption><thead><tr><th scope="col"><span class="sr-only">Feature</span></th><th scope="col">Free<small>€0</small></th><th scope="col" class="is-recommended" v-if="profilePackPlan()">Paper pack<small>{{ billingPrice(profilePackPlan()) }}</small></th><th scope="col" v-if="profileAuthorPlans().length">Author<small>{{ billingPrice(profileSelectedPlan()) }}</small></th></tr></thead><tbody>
<tr><th scope="row">Repositories</th><td>{{ billing.allowance?.repositories }}</td><td class="is-recommended" v-if="profilePackPlan()">+1</td><td v-if="profileAuthorPlans().length">{{ profileSelectedPlan()?.repositories }}</td></tr>
<tr><th scope="row">Source storage</th><td>{{ billingBytes(billing.allowance?.storage) }} total</td><td class="is-recommended" v-if="profilePackPlan()">{{ billingBytes(profileSlotPlan()?.storage || profilePassPlan()?.storage) }} for it</td><td v-if="profileAuthorPlans().length">{{ billingBytes(profileSelectedPlan()?.storage) }} total</td></tr>
<tr><th scope="row">Latest expiration</th><td>{{ billing.freeExpirationMonths || 5 }} months ahead</td><td class="is-recommended" v-if="profilePackPlan()">1 year after purchase</td><td v-if="profileAuthorPlans().length">Never</td></tr>
<tr><th scope="row">Automatic updates</th><td>No</td><td class="is-recommended" v-if="profilePackPlan()">180 days</td><td v-if="profileAuthorPlans().length">Yes</td></tr>
<tr><th scope="row">Renews</th><td>No</td><td class="is-recommended" v-if="profilePackPlan()">No</td><td v-if="profileAuthorPlans().length">Until cancelled</td></tr>
<tr><th scope="row">Reviewer access</th><td>Free</td><td class="is-recommended" v-if="profilePackPlan()">Free</td><td v-if="profileAuthorPlans().length">Free</td></tr>
</tbody></table></div>
</details>
<p class="billing-footnote" v-if="billing.checkoutEnabled === false">New purchases are not available right now. Existing billing can still be managed below.</p>
</section>
<section class="billing-section" v-if="billing?.enabled">
<div class="billing-section-head"><h2 class="billing-section-title">Purchases</h2><button type="button" class="settings-text-button" v-if="!billingPending" @click="refreshBilling()" :disabled="billingBusy">Refresh payment status</button></div>
<p class="billing-empty" v-if="!(billing.purchases || []).length">No purchases yet.</p>
<div class="billing-history-head" v-if="(billing.purchases || []).length" aria-hidden="true"><span>Purchase</span><span>Period</span><span>Amount</span><span>Status</span></div>
<ul class="billing-history" v-if="(billing.purchases || []).length">
<li class="billing-history-row" v-for="purchase in profilePurchases(true).concat(profilePurchases(false))" :key="purchase.id" :class="{ 'is-ended': !['active', 'overdue'].includes(purchase.status) }">
<div class="billing-history-main">
<strong>{{ profileGrantName(purchase) }}<template v-if="purchase.repoId"> · <a :href="'/anonymize/' + encodeURIComponent(purchase.repoId)">{{ purchase.repoId }}</a></template><template v-else-if="purchase.conference"> · <a :href="'/conference/' + encodeURIComponent(purchase.conference)">{{ purchase.conference }}</a></template></strong>
<span>{{ [purchase.purchasedAt ? 'Bought ' + billingDate(purchase.purchasedAt) : '', purchase.parts.length ? profilePackSummary(purchase) : ['refunded', 'disputed'].includes(purchase.status) ? 'Payment ' + purchase.status + ', so what it paid for has ended' : purchase.recurring ? 'Account subscription' : ''].filter(Boolean).join(' · ') }}</span>
</div>
<div class="billing-history-term">{{ profilePurchaseTerm(purchase) }}</div>
<div class="billing-history-paid">{{ purchase.amount != null ? billingPrice(purchase) : '' }}</div>
<div class="billing-history-status"><span class="settings-status" :class="'is-' + purchase.status">{{ profilePurchaseStatus(purchase) }}</span></div>
</li>
</ul>
<div class="billing-manage">
<div><strong>Invoices and payment details</strong><p v-if="billing.portal">Download receipts, update your card or change renewal in Stripe.</p><p v-else>Invoices appear here after your first purchase.</p>
<p>Need a refund? <a href="mailto:tdurieux@anonymous.4open.science">Email the maintainer</a> with the payment date, or read the <a href="/faq#faq-plans">refund policy</a>.</p></div>
<button type="button" class="btn btn-outline-ink" v-if="billing.portal" @click="billingPortal()" :disabled="billingBusy">Manage billing and invoices</button>
</div>
</section>
<p class="billing-conference">Organizing a conference? <template v-if="profileConferencePlan()">Review packages start at {{ billingAmount(profileConferencePlan()) }} once for up to {{ profileConferencePlan().repositories }} submissions, and authors pay nothing.</template><template v-else>One payment hosts every submission, and authors pay nothing.</template> <a href="/conference/new">Create a conference</a></p>
</section>
<section id="settings-account" class="settings-account-panel" v-show="settingsTab === 'account'">
<p class="settings-scope">Your identity, repository access, and account.</p>
+196 -26
View File
@@ -1,4 +1,4 @@
import { setupBilling, readBilling } from "./billing.js";
import { setupBilling, readBilling, isRepositoryAddon } from "./billing.js";
import { reactive, nextTick, markRaw } from "vue";
import { loadEditor } from "./lazy-assets.js";
import { createTimers, createListeners } from "./state.js";
@@ -193,7 +193,7 @@ export const mainController = function (state, http, location, timeout) {
state.previewTabKeydown = previewTabKeydown;
state.headerCanUpgrade = false;
state.headerShowSupport = false;
state.headerBillingPending = ["success", "portal"].includes(new URLSearchParams(window.location.search).get("billing"));
state.headerBillingPending = new URLSearchParams(window.location.search).get("billing") === "success";
const updateHeaderBilling = billing => {
state.headerShowSupport = !(billing?.grants || []).some(g => ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now());
const subscribed = (billing?.grants || []).some(g => ["author_monthly", "author_annual"].includes(g.planId) && ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now());
@@ -372,7 +372,8 @@ export const profileController = function (state, http, translate, timeout, quot
state.profilePastSubscription = () => !state.profileAuthorGrant() && (state.billing?.grants || []).some(g => ["author_monthly", "author_annual"].includes(g.planId));
state.showAuthorPlans = new URLSearchParams(window.location.search).get("upgrade") === "1";
state.showRepositoryPass = false;
state.profilePlan = "author_monthly";
// Yearly is the better deal, so it is shown first.
state.profilePlan = "author_annual";
state.openProfileUpgrade = () => {
state.settingsTab = "billing";
state.showAuthorPlans = true;
@@ -382,10 +383,78 @@ export const profileController = function (state, http, translate, timeout, quot
state.profileAuthorPlans = () => (state.billing?.plans || []).filter(p => ["author_monthly", "author_annual"].includes(p.id));
state.profileSelectedPlan = () => state.profileAuthorPlans().find(p => p.id === state.profilePlan) || state.profileAuthorPlans()[0];
state.profilePassPlan = () => (state.billing?.plans || []).find(p => p.id === "author_pass");
// The Paper pack covers the usual case, so it is listed first.
state.profileAddonPlans = () => (state.billing?.plans || []).filter(p => isRepositoryAddon(p.id))
.sort((a, b) => Number(b.id === "repo_pack") - Number(a.id === "repo_pack"));
state.profileAddonId = "repo_pack";
state.profilePackPlan = () => (state.billing?.plans || []).find(p => p.id === "repo_pack");
state.profileSlotPlan = () => (state.billing?.plans || []).find(p => p.id === "repo_slot");
state.profileRepositories = () => (state.billing?.repositories || []).filter(r => r.status !== "removed");
state.openProfileAddon = (planId) => {
if (planId) state.profileAddonId = planId;
state.showRepositoryPass = true;
state.billingRepository = "";
nextTick(() => {
const panel = document.getElementById("settings-addons-title");
panel?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
panel?.focus?.({ preventScroll: true });
});
};
const addonOrder = ["repo_slot", "author_pass", "repo_extend", "repo_never"];
state.profileSingleAddons = () => state.profileAddonPlans().filter(p => p.id !== "repo_pack")
.sort((a, b) => addonOrder.indexOf(a.id) - addonOrder.indexOf(b.id));
state.profileSingleId = "repo_slot";
state.profileSingleAddon = () => state.profileSingleAddons().find(p => p.id === state.profileSingleId) || state.profileSingleAddons()[0];
state.profileCheapestAddon = () => [...state.profileSingleAddons()].sort((a, b) => (a.amount ?? Infinity) - (b.amount ?? Infinity))[0];
state.profileAuthorPlansByValue = () => [...state.profileAuthorPlans()].sort((a, b) => Number(a.interval === "year") - Number(b.interval === "year"));
// One line for a pack's three parts.
state.profilePackSummary = p => p.parts.map(part => part.planId === "repo_slot" ? "Extra repository"
: `${state.profileGrantName(part)} until ${state.billingDate(part.until)}`).join(" · ");
state.profileAddonFeatures = id => ({
repo_pack: ["One more repository, outside your free ones", `Its own ${state.billingBytes(state.profileSlotPlan()?.storage || state.profilePassPlan()?.storage)} of storage`, "Automatic updates for 180 days", "Online for up to a year", "No subscription"],
repo_slot: ["One more repository, outside your free ones", `Its own ${state.billingBytes(state.profileSlotPlan()?.storage)} of storage`, "Valid while the repository exists", "Refresh it by hand"],
author_pass: ["Automatic updates for 180 days", `Its own ${state.billingBytes(state.profilePassPlan()?.storage)} of storage`, "No subscription"],
repo_extend: ["Online for up to one more year", "Buy again to add another year", "No subscription"],
repo_never: ["The link never expires", "No subscription", "Ends only if the payment is refunded"],
}[id] || []);
state.profileAddonBlurb = id => ({ repo_slot: "One more repository", author_pass: "Auto-updates for 180 days",
repo_extend: "Online one more year", repo_never: "The link never expires" }[id] || "");
state.profileAuthorPurchase = () => (state.billing?.purchases || []).find(p => p.recurring && ["active", "overdue"].includes(p.status));
state.profileRepositoryExpiry = repo => repo.status === "expired" ? "expired"
: repo.expirationMode === "never" ? "never expires" : repo.expirationDate ? "expires " + state.billingDate(repo.expirationDate) : "no date";
// Annual saving against twelve monthly payments, in whole percent.
state.profileAnnualSavings = () => {
const monthly = state.profileAuthorPlans().find(p => p.interval === "month")?.amount;
const annual = state.profileAuthorPlans().find(p => p.interval === "year")?.amount;
return monthly && annual && annual < monthly * 12 ? Math.round((1 - annual / (monthly * 12)) * 100) : 0;
};
state.profilePurchases = active => (state.billing?.purchases || []).filter(p => ["active", "overdue"].includes(p.status) === active);
state.profilePurchaseStatus = p => ({ active: "Active", overdue: "Payment overdue", ended: "Ended", refunded: "Refunded", disputed: "Disputed" }[p.status] || p.status);
// Plain wording for how long a purchase lasts; the "no end" sentinel is never shown.
state.profilePurchaseTerm = p => {
const ended = p.status && !["active", "overdue"].includes(p.status);
if (p.recurring) return ended ? (p.until ? "Ended " + state.billingDate(p.until) : "") : (p.cancelAtPeriodEnd ? "Ends " : "Renews ") + state.billingDate(p.until);
if (p.planId === "repo_pack") {
const dates = p.parts.map(part => part.until).filter(Boolean).map(d => new Date(d).getTime());
return ended || !dates.length ? "" : "Until " + state.billingDate(new Date(Math.max(...dates)));
}
if (!p.until) return ended ? "" : p.planId === "repo_slot" ? "While the repository exists" : "No expiration";
return (ended || new Date(p.until).getTime() < Date.now() ? "Ended " : "Until ") + state.billingDate(p.until);
};
state.profileAddon = () => state.profileAddonPlans().find(p => p.id === state.profileAddonId) || state.profileAddonPlans()[0];
state.profileAuthorGrant = () => (state.billing?.grants || []).find(g => ["author_monthly", "author_annual"].includes(g.planId) && ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now());
state.profileGrantName = grant => ({ author_monthly: "Author monthly", author_annual: "Author annual", author_pass: "Repository pass", conference: "Conference hosting" }[grant.planId] || grant.planId);
// The cheapest conference package, for the "from" price.
state.profileConferencePlan = () => (state.billing?.plans || []).filter(p => ["conference_workshop", "conference", "conference_large"].includes(p.id) && p.amount != null)
.sort((a, b) => a.amount - b.amount)[0];
state.profileGrantName = grant => ({ author_monthly: "Author monthly", author_annual: "Author annual", author_pass: "Live updates", repo_extend: "Extended expiration", repo_never: "Never expire", repo_slot: "Extra repository", repo_pack: "Paper pack", conference: "Conference package", conference_workshop: "Workshop package", conference_large: "Large conference package" }[grant.planId] || grant.planId);
setupBilling(state, http, () => quotaService.load().then(quota => { state.quota = quota; }, console.error));
setupBilling(state, http, () => quotaService.load().then(quota => { state.quota = quota; }, console.error), {
portalMessage: () => {
const grant = state.profileAuthorGrant();
if (!grant) return "Your billing details are up to date.";
return grant.cancelAtPeriodEnd ? `Renewal cancelled. Your access continues until ${state.billingDate(grant.until)}.` : `Your subscription renews on ${state.billingDate(grant.until)}.`;
},
});
state.terms = "";
state.options = {
expirationMode: "remove",
@@ -1232,27 +1301,30 @@ export const unifiedDashboardController = function (state, http, location, promi
const toast = reactive({
title: `Extending ${item._id}...`,
date: new Date(),
body: `The expiration of ${label} ${item._id} is going to be extended by 6 months.`,
body: `The expiration of ${label} ${item._id} is being extended.`,
});
state.addToast(toast);
const endpoint = `${apiBaseOf(item._type)}/${item._id}/extend`;
http.post(endpoint).then(
() => {
(res) => {
const until = res?.data?.expirationDate ? ` until ${new Date(res.data.expirationDate).toLocaleDateString()}` : "";
if (item._type === "repo") {
waitRepoToBeReady(item._id, () => {
toast.title = `${item._id} is extended.`;
toast.body = `The expiration of ${label} ${item._id} is extended by 6 months.`;
toast.body = `${label} ${item._id} is extended${until}.`;
}, undefined, item.status);
} else {
toast.title = `${item._id} is extended.`;
toast.body = `The expiration of ${label} ${item._id} is extended by 6 months.`;
toast.body = `${label} ${item._id} is extended${until}.`;
loadAll();
}
},
(error) => {
toast.title = `Error during the extension of ${item._id}.`;
toast.body = (error.data && error.data.error) || error.body;
const code = error.data && error.data.error;
toast.body = code || error.body;
if (code && Object.prototype.hasOwnProperty.call(translations.ERRORS, code)) toast.body = translations.ERRORS[code];
loadAll();
}
);
@@ -1447,7 +1519,7 @@ export const anonymizeController = function (state, http, html, params, location
state.editRepository();
return;
}
if (state.hostingNeedsPayment()) return state.checkoutRepository();
if (state.hostingNeedsPayment()) return state.checkoutRepository(isRepositoryAddon(state.hostingChoice()) ? state.hostingChoice() : "");
return state.anonymizeRepo(event);
};
state.hostingPayment = false;
@@ -1457,9 +1529,9 @@ export const anonymizeController = function (state, http, html, params, location
const amount = new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency, minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(plan.amount / 100);
return amount + (plan.interval === "year" ? " billed annually" : " billed monthly");
};
state.hostingPlan = "author_monthly";
state.hostingPlan = "";
state.hostingSaving = false;
state.selectedHostingPlan = () => state.authorPlans().find(p => p.id === state.hostingPlan) || state.authorPlans()[0];
state.selectedHostingPlan = () => state.authorPlans().find(p => p.id === state.hostingPlan) || state.hostingSubscriptionPlan?.();
const loadHostingQuota = () => http.get("/api/user/quota").then(res => { state.hostingQuota = res.data; });
setupBilling(state, http, async () => {
await loadHostingQuota();
@@ -1467,15 +1539,59 @@ export const anonymizeController = function (state, http, html, params, location
}, { confirmedMessage: "Hosting status refreshed. Your draft is ready to continue." });
loadHostingQuota().catch(() => {});
state.authorPlans = () => (state.billing?.plans || []).filter(p => ["author_monthly", "author_annual"].includes(p.id));
// A paid, open conference hosts the submission instead of the author.
state.conferenceHosting = () => !!(state.conference && state.conference_data?.hosting);
state.hasAuthorHosting = () => ["author_monthly", "author_annual"].includes(state.billing?.allowance?.plan);
state.hostingNeedsPayment = () => {
if (!state.billing?.enabled || state.detectedType !== "repo" || state.hasAuthorHosting() ||
(state.isUpdate && state.role !== "owner")) return false;
const needsLifetime = state.options?.expirationMode === "never" && !state._existingNever;
const needsLifetime = state.options?.expirationMode === "never" && !state._existingNever && !state.repositoryGrants("repo_never").length;
// Reported usage may include removed reservations; admission reclaims them.
return !!(needsLifetime || (!state.conference && state.hostingQuotaBlocked));
return !!(needsLifetime || (!state.conferenceHosting() && state.hostingQuotaBlocked));
};
state.checkoutRepository = async (pass = false) => {
// One-time add-ons the current user bought for this repository.
state.repositoryGrants = planId => (state.billing?.grants || []).filter(g => g.planId === planId && g.scope === `repo:${state.repoId}` &&
["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now());
state.repositoryAddonPlans = () => (state.billing?.plans || []).filter(p => isRepositoryAddon(p.id) &&
(["repo_extend", "repo_pack"].includes(p.id) || !state.repositoryGrants(p.id).length));
// One-time add-ons that resolve this submission: Never expire for a
// never-expiring link, otherwise the paper pack or an extra repository
// when the free allowance is full. The first is preselected.
state.hostingOneTimePlans = () => {
const offered = ids => ids.map(id => (state.billing?.plans || []).find(p => p.id === id && !state.repositoryGrants(id).length)).filter(Boolean);
if (state.options?.expirationMode === "never" && !state._existingNever && !state.repositoryGrants("repo_never").length) return offered(["repo_never"]);
return state.hostingQuotaBlocked && !state.conferenceHosting() ? offered(["repo_slot", "repo_pack"]) : [];
};
state.hostingOneTime = () => state.hostingOneTimePlans()[0];
state.neverExpireTag = () => {
if (state.hasAuthorHosting()) return "Included in your subscription";
if (state.repositoryGrants("repo_never").length) return "Paid for this repository";
const plan = (state.billing?.plans || []).find(p => p.id === "repo_never");
return plan?.amount != null ? `${state.billingAmount(plan)} once, or with a subscription` : "Subscription or one-time add-on";
};
// Annual before monthly: it is the cheaper subscription.
// Subscriptions appear as one option with a monthly/yearly switch.
state.hostingCycle = "author_annual";
state.hostingSubscriptionPlan = () => state.authorPlans().find(p => p.id === state.hostingCycle) || state.authorPlans().find(p => p.interval === "year") || state.authorPlans()[0];
state.hostingAnnualSavings = () => {
const monthly = state.authorPlans().find(p => p.interval === "month")?.amount;
const annual = state.authorPlans().find(p => p.interval === "year")?.amount;
return monthly && annual && annual < monthly * 12 ? Math.round((1 - annual / (monthly * 12)) * 100) : 0;
};
state.hostingOptionPlans = () => state.hostingOneTimePlans().concat(state.authorPlans().length ? [state.hostingSubscriptionPlan()] : []);
state.hostingOptionName = plan => ({ repo_slot: "Add this repository", repo_pack: "Paper pack", repo_never: "Never expire",
author_annual: "Author subscription", author_monthly: "Author subscription" }[plan.id] || plan.name);
state.hostingOptionBlurb = plan => ({
repo_slot: "Hosted outside your free repositories. Refresh it by hand.",
repo_pack: "Adds automatic updates for 180 days and keeps the link online for a year.",
repo_never: "This link never expires. No subscription.",
}[plan.id] || `${plan.repositories} repositories, ${state.billingBytes(plan.storage)}, automatic updates and links that never expire.`);
state.hostingChoice = () => {
const oneTime = state.hostingOneTimePlans();
if (oneTime.some(p => p.id === state.hostingPlan) || state.authorPlans().some(p => p.id === state.hostingPlan)) return state.hostingPlan;
return oneTime[0]?.id || state.hostingSubscriptionPlan()?.id || "";
};
state.checkoutRepository = async (addon = "") => {
if (state.hostingSaving || state.billingBusy || !state.billing?.csrf || state.sourceLoading) return;
state.hostingSaving = true;
state.billingError = "";
@@ -1487,9 +1603,10 @@ export const anonymizeController = function (state, http, html, params, location
checkoutDraftId = saved.data.id;
checkoutDraftSnapshot = serialized;
}
const plan = state.authorPlans().find(p => p.id === state.hostingPlan) || state.authorPlans()[0];
if (!pass && !plan) throw new Error("No author plan available");
await state.checkout(pass ? "author_pass" : plan.id, pass ? state.repoId : "", { draftId: checkoutDraftId });
// The subscription option shows one cycle; Checkout must open that same cycle.
const plan = state.authorPlans().find(p => p.id === state.hostingPlan) || state.hostingSubscriptionPlan();
if (!addon && !plan) throw new Error("No author plan available");
await state.checkout(addon || plan.id, addon ? state.repoId : "", { draftId: checkoutDraftId });
} catch { state.billingError = "Your draft could not be saved for checkout. Your form is unchanged. Please try again."; }
finally { state.hostingSaving = false; }
};
@@ -1582,12 +1699,33 @@ export const anonymizeController = function (state, http, html, params, location
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
// Bound the expiration date: no earlier than today, no later than 1 year
// out.
// Bound the expiration date: no earlier than today. The latest date
// mirrors the server: a year without billing or with an author
// subscription, otherwise the free window, extended by this repository's
// Extended expiration add-ons or its conference's review window. A saved
// later date stays selectable.
state.minExpirationDate = toLocalDateString(new Date());
const maxDate = new Date();
maxDate.setFullYear(maxDate.getFullYear() + 1);
state.maxExpirationDate = toLocalDateString(maxDate);
const updateExpirationLimit = () => {
const now = new Date();
let maximum = new Date(now);
maximum.setFullYear(maximum.getFullYear() + 1);
// Gists and pull requests keep their one-year limit; billing only limits repositories.
if (state.billing?.enabled && state.detectedType === "repo" && !state.hasAuthorHosting()) {
maximum = new Date(now);
maximum.setMonth(maximum.getMonth() + (state.billing.freeExpirationMonths || 5));
for (const grant of state.repositoryGrants("repo_extend")) if (new Date(grant.until) > maximum) maximum = new Date(grant.until);
if (state.conference_data?.endDate && new Date(state.conference_data.endDate) > maximum) maximum = new Date(state.conference_data.endDate);
if (state._savedExpirationDate && state._savedExpirationDate > maximum) maximum = new Date(state._savedExpirationDate);
// A default or carried-over date must fit the limit; only the saved
// date may stay later.
const date = state.options?.expirationDate;
const saved = state._savedExpirationDate && date instanceof Date && date.getTime() === state._savedExpirationDate.getTime();
if (date instanceof Date && date > maximum && !saved) state.options.expirationDate = new Date(maximum);
}
state.maxExpirationDate = toLocalDateString(maximum);
};
updateExpirationLimit();
state.watch(() => [state.billing, state.repoId, state.conference_data, state.isUpdate, state._savedExpirationDate, state.options?.expirationMode, state.detectedType], updateExpirationLimit, true, false);
state.anonymize_readme = "";
state.readme = "";
state.html_readme = "";
@@ -1774,6 +1912,7 @@ export const anonymizeController = function (state, http, html, params, location
state.source = res.data.source;
state.role = res.data.role || "owner";
state._existingNever = res.data.options.expirationMode === "never";
state._savedExpirationDate = res.data.options.expirationDate ? new Date(res.data.options.expirationDate) : null;
state.coauthors = res.data.coauthors || [];
// Remember the saved branch so the source.branch watcher knows
// not to bump source.commit to GitHub HEAD on edit-page load
@@ -2689,9 +2828,11 @@ export const anonymizeController = function (state, http, html, params, location
() => { draft.clear(); window.location.href = "/status/" + state.repoId; },
(error) => {
if (error.data) {
translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error);
// The hosting section explains a hosting limit and offers the fix.
const hostingLimit = ["quota_exceeded", "subscription_required_for_never_expiration"].includes(error.data.error);
if (!hostingLimit) translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error);
displayErrorMessage(error.data.error);
if (["quota_exceeded", "subscription_required_for_never_expiration"].includes(error.data.error)) {
if (hostingLimit) {
if (error.data.error === "subscription_required_for_never_expiration") state._existingNever = false;
state.hostingQuotaBlocked = true;
state.hostingReview = true;
@@ -4041,6 +4182,26 @@ export const newConferenceController = function (state, http, location, params)
}
state.plans = [];
state.plansLoaded = false;
// With billing on, the review packages (one per venue size) are the only
// plans; their prices come from Stripe through the billing endpoint.
state.conferenceBilling = null;
readBilling(http).then(res => { state.conferenceBilling = res.data; }).catch(() => {});
state.conferenceTiers = () => state.plans.filter(p => p.id !== "free_conference").map(p => {
const priced = (state.conferenceBilling?.plans || []).find(b => b.id === p.id) || {};
return { ...p, storage: p.storagePerRepo, ...priced, id: p.id, name: p.name };
});
state.conferenceAudience = id => ({ conference_workshop: "Workshops and small venues", conference: "A typical conference", conference_large: "Large or multi-track venues" }[id] || "");
state.conferencePlanName = id => conferencePlanName(id);
state.conferenceAmount = (plan, cents = plan?.amount) => {
return new Intl.NumberFormat(undefined, { style: "currency", currency: plan?.currency || "eur",
minimumFractionDigits: cents % 100 ? 2 : 0, maximumFractionDigits: 2 }).format(cents / 100);
};
state.conferenceBytes = value => {
const bytes = Number(value) || 0;
const exponent = Math.min(4, Math.floor(Math.log2(Math.max(bytes, 1)) / 10));
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KiB", "MiB", "GiB", "TiB"][exponent]}`;
};
state.editionMode = false;
state.saving = false;
state.error = "";
@@ -4062,6 +4223,10 @@ export const newConferenceController = function (state, http, location, params)
function getPlans() {
http.get("/api/conferences/plans").then((res) => {
state.plans = res.data;
state.plansLoaded = true;
// New conferences default to the middle package when it is offered.
if (!state.editionMode && state.plans.length && !state.plans.some(p => p.id == state.options.plan.planID))
state.options.plan.planID = (state.plans.find(p => p.id === "conference") || state.plans[0]).id;
state.plan = state.plans.filter(
(f) => f.id == state.options.plan.planID
@@ -4184,8 +4349,13 @@ export const newConferenceController = function (state, http, location, params)
};
};
function conferencePlanName(id) {
return { free_conference: "Free (no longer offered to new conferences)", conference_workshop: "Workshop package", conference: "Conference package", conference_large: "Large conference package" }[id] || id || "Free";
}
export const conferenceController = function (state, http, location, params) {
setupBilling(state, http, () => getConference(), { confirmedMessage: "Payment status checked. Your conference is ready for submissions once payment is confirmed." });
state.conferencePlanName = id => conferencePlanName(id);
state.watch("user.status", () => {
if (state.user == null) {
location.url("/");
+19 -5
View File
@@ -1,3 +1,5 @@
// One-time purchases that apply to a single repository.
export function isRepositoryAddon(planId) { return ["author_pass", "repo_extend", "repo_never", "repo_slot", "repo_pack"].includes(planId); }
// Share concurrent catalog reads between the app header and the current page.
const billingReads = new WeakMap();
export function readBilling(http) {
@@ -14,7 +16,9 @@ export function setupBilling(state, http, onRefresh = () => {}, context = {}) {
state.billingBusy = false;
state.billingError = "";
state.billingNotice = "";
state.billingPending = outcome === "success" || outcome === "portal";
// Only a completed checkout waits for payment confirmation. Returning from
// the billing portal (cancelling renewal, downloading an invoice) does not.
state.billingPending = outcome === "success";
state.billingRepository = "";
const messages = {
billing_invalid_draft: "This draft could not be saved. Your form is unchanged. Try again.",
@@ -23,18 +27,22 @@ export function setupBilling(state, http, onRefresh = () => {}, context = {}) {
billing_checkout_disabled: "New purchases are temporarily unavailable. You can still manage existing billing.",
billing_unavailable: "Payments are not available yet. Please try again later.",
billing_subscription_exists: "You already have a subscription. Use Manage billing to change or cancel it.",
billing_checkout_in_progress: "You have another checkout open. Complete it or wait for it to expire before choosing a different plan.",
billing_checkout_in_progress: "You have another checkout open. Complete it, or wait up to 30 minutes for it to expire, before choosing a different option.",
billing_busy: "Another billing request is processing. Please try again shortly.",
billing_already_paid: "This repository or conference already has an active paid package.",
billing_already_paid: "This repository or conference already has this paid option.",
billing_invalid_review_window: "Conference packages cover up to 180 days. Check the review dates.",
billing_invalid_conference: "Only a conference owner can pay for a conference awaiting payment.",
repo_not_found: "Enter the ID of an existing repository that you own.",
};
// Whole amounts read as "€5", not "€5.00".
state.billingAmount = plan => plan?.amount == null ? "" : new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency,
minimumFractionDigits: plan.amount % 100 ? 2 : 0, maximumFractionDigits: 2 }).format(plan.amount / 100);
state.billingPrice = plan => {
if (plan.amount == null) return "Price shown at checkout";
const amount = new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency }).format(plan.amount / 100);
const amount = state.billingAmount(plan);
return amount + (plan.interval ? ` / ${plan.intervalCount > 1 ? plan.intervalCount + " " : ""}${plan.interval}` : " once");
};
state.billingPerMonth = plan => plan?.amount == null ? "" : new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency }).format(plan.amount / 1200);
state.billingBytes = value => {
const bytes = Number(value) || 0;
if (bytes < 1024) return `${bytes} B`;
@@ -75,9 +83,15 @@ export function setupBilling(state, http, onRefresh = () => {}, context = {}) {
state.billingNotice = "Confirmation is taking longer than usual. You can refresh payment status later. Please do not pay again.";
}
state.loadBilling().then(() => {
if (outcome === "success" || outcome === "portal") {
if (outcome === "success") {
state.billingNotice = "Checking your payment. Paid access starts after confirmation.";
confirmPayment();
} else if (outcome === "portal") {
state.refreshBilling().then(() => {
state.billingPending = false;
state.emit?.("billing:pending", false);
state.billingNotice = context.portalMessage?.(state.billing) || "Your billing details are up to date.";
});
} else if (outcome === "cancelled") state.billingNotice = "Checkout was closed. No new paid access has been activated.";
});
}
+40 -40
View File
@@ -1,44 +1,44 @@
"use strict";(()=>{var va=Object.defineProperty;var ya=e=>{throw TypeError(e)};var ip=(e,t,s)=>t in e?va(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var rp=(e,t)=>{for(var s in t)va(e,s,{get:t[s],enumerable:!0})};var We=(e,t,s)=>ip(e,typeof t!="symbol"?t+"":t,s),$i=(e,t,s)=>t.has(e)||ya("Cannot "+s);var oe=(e,t,s)=>($i(e,t,"read from private field"),s?s.call(e):t.get(e)),Ie=(e,t,s)=>t.has(e)?ya("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,s),qe=(e,t,s,i)=>($i(e,t,"write to private field"),i?i.call(e,s):t.set(e,s),s),Me=(e,t,s)=>($i(e,t,"access private method"),s);var ba=(e,t,s,i)=>({set _(o){qe(e,t,o,s)},get _(){return oe(e,t,i)}});var ge=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={markdown:"/script/markdown.6c8dfb7b1d.min.js",org:"/script/org.f61f262915.min.js",notebook:"/script/notebook.c3144c18ae.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};function wa(e){let t=e.location.hash.slice(1);if(e.history.replaceState(null,"","/review-link"),t.length>2048)return null;let s=new URLSearchParams(t);if([...s.keys()].length!==3||[...s.keys()].some(a=>!["clientId","intentId","token"].includes(a)))return null;let i=s.get("clientId"),o=s.get("intentId"),r=s.get("token");return!/^[a-f0-9]{32}$/.test(i||"")||!/^[a-f0-9]{32}$/.test(o||"")||!/^[a-f0-9]{64}$/.test(r||"")?null:{contract:"4open.artifacts/1",clientId:i,intentId:o,token:r}}function ka(e,t){let{http:s,promises:i,window:o}=t,r=t.takeReviewHandoff(),a=r?{clientId:r.clientId,intentId:r.intentId}:null,u,c,m,g,v,h=!0,k,A=i.defer(),w=()=>[...o.crypto.getRandomValues(new Uint8Array(16))].map(q=>q.toString(16).padStart(2,"0")).join("");Object.assign(e,{repositoryId:"",handoffEnabled:!1,busy:!1,preview:null,acceptAccess:!1,acceptRetention:!1,uncertain:!1,saved:!1,expired:!1,error:r?"":"Open a new artifact link from your submission. This link is missing or invalid."});let b=()=>h&&e.user?.username===v;function E(){h=!1,r=u=c=void 0,A.resolve(),o.clearTimeout(k)}e.on("dispose",E),e.on("routeLeave",E),e.watch(()=>e.user?.username,q=>{!v&&q?v=q:v&&q!==v&&(E(),e.preview=null,e.repositoryId="",e.busy=!1,e.saved=!1,e.handoffEnabled=!1,e.error="Your account changed. Open a new artifact link from your submission.")});let N=()=>({timeout:A.promise,headers:{"Content-Type":"application/json","X-Review-CSRF":c}}),O=q=>q?.status===404?"Artifact linking is not available on this server.":q?.status===401?"Sign in, then open a new artifact link from your submission.":q?.status===403?"This account cannot approve this repository or this link.":"The request could not be completed. You can retry from this page.";e.loadPreview=async()=>{if(!h||!r||e.busy||e.uncertain||e.saved||!e.user?.username)return;v=e.user.username;let q=e.repositoryId.trim();if(!/^[A-Za-z0-9_-]{3,128}$/.test(q)){e.error="Enter the anonymous repository ID from its URL.";return}e.busy=!0,e.error="",e.preview=null,u=void 0,e.acceptAccess=e.acceptRetention=!1,e.expired=!1,o.clearTimeout(k),g||(g=w());try{let I=(await s.get("/api/review-consent/csrf",{timeout:A.promise})).data;if(!b())return;if(c=I.csrf,e.handoffEnabled=I.handoffEnabled===!0,!/^[a-f0-9]{64}$/.test(c||""))throw new Error("invalid_csrf");let j=(await s.post("/api/review-consent/preview",{repositoryId:q,intent:{...r,requestId:g}},N())).data;if(!b())return;if(!j||typeof j.ticket!="string"||!j.ticket||j.ticket.length>8192||typeof j.repositoryName!="string"||!["restricted-review","anonymous-link"].iLine truncated
`))),e[on]=bs(o);let r=i||o.props&&o.props.type==="number";Sn(e,t?"change":"input",a=>{a.target.composing||e[on](Er(e.value,s,r))}),(s||r)&&Sn(e,"change",()=>{e.value=Er(e.value,s,r)}),t||(Sn(e,"compositionstart",uh),Sn(e,"compositionend",ad),Sn(e,"change",ad))},mounted(e,{value:t,modifiers:{trim:s,number:i}}){let o=t??"",r=e[pi];delete e[pi],r!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==r?e[on](Er(e.value,s,i)):e.value=o},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:i,trim:o,number:r}},a){if(e[on]=bs(a),e.composing)return;let u=(r||e.type==="number")&&!/^0\d/.test(e.value)?cs(e.value):e.value,c=t??"";if(u===c)return;let m=e.getRootNode();(m instanceof Document||m instanceof ShadowRoot)&&m.activeElement===e&&e.type!=="range"&&(i&&t===s||o&&e.value.trim()===c)||(e.value=c)}},Nr={deep:!0,created(e,t,s){e[on]=bs(s),Sn(e,"change",()=>{let i=e._modelValue,o=ho(e),r=e.checked,a=e[on];if(Se(i)){let u=Fo(i,o),c=u!==-1;if(r&&!c)a(i.concat(o));else if(!r&&c){let m=[...i];m.splice(u,1),a(m)}}else if(Ut(i)){let u=new Set(i);r?u.add(o):u.delete(o),a(u)}else a(fd(e,r))})},mounted:ld,beforeUpdate(e,t,s){e[on]=bs(s),ld(e,t,s)}};function ld(e,{value:t,oldValue:s},i){e._modelValue=t;let o;if(Se(t))o=Fo(t,i.props.value)>-1;else if(Ut(t))o=t.has(i.props.value);else{if(t===s)return;o=Ft(t,fd(e,!0))}e.checked!==o&&(e.checked=o)}var Wn={deep:!0,created(e,{value:t,modifiers:{number:s}},i){e._modelValue=t,Sn(e,"change",()=>{let o=Array.prototype.filter.call(e.options,c=>c.selected).map(c=>s?cs(ho(c)):ho(c)),r=e.multiple,a=r?Ut(e._modelValue)?new Set(o):o:o[0],u=e._pendingValue=[r,r?Se(a)?o.slice():o:a];try{e[on](a)}finally{it(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[on]=bs(i)},mounted(e,{value:t}){dd(e,t)},beforeUpdate(e,{value:t},s){e._modelValue=t,e[on]=bs(s)},updated(e,{value:t}){let s=e._pendingValue;e._pendingValue=void 0,(!s||s[0]!==e.multiple||!ch(t,s[1],s[0]))&&dd(e,t)}};function ch(e,t,s){if(!s)return Ft(e,t);if(Se(e))return Ft(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let i of t)if(!e.has(i))return!1;return!0}return!1}function dd(e,t){let s=e.multiple,i=Se(t);if(!(s&&!i&&!Ut(t))){for(let o=0,r=e.options.length;o<r;o++){let a=e.options[o],u=ho(a);if(s)if(i){let c=typeof u;c==="string"||c==="number"?a.selected=t.some(m=>String(m)===String(u)):a.selected=Fo(t,u)>-1}else a.selected=t.has(u);else if(Ft(ho(a),t)){e.selectedIndex!==o&&(e.selectedIndex=o);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function ho(e){return"_value"in e?e._value:e.value}function fd(e,t){let s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}var ph=["ctrl","shift","alt","meta"],fh={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>ph.some(s=>e[`${s}Key`]&&!t.includes(s))},ce=(e,t)=>{if(!e)return e;let s=e._withMods||(e._withMods={}),i=t.join(".");return s[i]||(s[i]=((o,...r)=>{for(let a=0;a<t.length;a++){let u=fh[t[a]];if(u&&u(o,t))return}return e(o,...r)}))},hh={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Tt=(e,t)=>{let s=e._withKeys||(e._withKeys={}),i=t.join(".");return s[i]||(s[i]=(o=>{if(!("key"in o))return;let r=Yt(o.key);if(t.some(a=>a===r||hh[a]===r))return e(o)}))},mh=ot({patchProp:ah},Gf),ud;function gh(){return ud||(ud=Hl(mh))}var hd=((...e)=>{let t=gh().createApp(...e),{mount:s}=t;return t.mount=i=>{let o=yh(i);if(!o)return;let r=t._component;!Ae(r)&&!r.render&&!r.template&&(r.template=o.innerHTML),o.nodeType===1&&(o.textContent="");let a=s(o,!1,vh(o));return o instanceof Element&&(o.removeAttribute("v-cloak"),o.setAttribute("data-v-app","")),a},t});function vh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function yh(e){return xe(e)?document.querySelector(e):e}var Xn=typeof document<"u";function gd(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function bh(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&gd(e.default)}var je=Object.assign;function hi(e,t){let s={};for(let i in t){let o=t[i];s[i]=Lt(o)?o.map(e):e(o)}return s}var ks=()=>{},Lt=Array.isArray;function Tr(e,t){let s={};for(let i in e)s[i]=i in t?t[i]:e[i];return s}var vd=/#/g,wh=/&/g,kh=/\//g,Ch=/=/g,Eh=/\?/g,yd=/\+/g,Sh=/%5B/g,Nh=/%5D/g,bd=/%5E/g,Rh=/%60/g,wd=/%7B/g,Dh=/%7C/g,kd=/%7D/g,Th=/%20/g;function Ar(e){return e==null?"":encodeURI(""+e).replace(Dh,"|").replace(Sh,"[").replace(Nh,"]")}function Cd(e){return Ar(e).replace(wd,"{").replace(kd,"}").replace(bd,"^")}function Rr(e){return Ar(e).replace(yd,"%2B").replace(Th,"+").replace(vd,"%23").replace(wh,"%26").replace(Rh,"`").replace(wd,"{").replace(kd,"}").replace(bd,"^")}function Ah(e){return Rr(e).replace(Ch,"%3D")}function _h(e){return ALine truncated
"use strict";(()=>{var ya=Object.defineProperty;var ba=e=>{throw TypeError(e)};var ap=(e,t,o)=>t in e?ya(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o;var lp=(e,t)=>{for(var o in t)ya(e,o,{get:t[o],enumerable:!0})};var We=(e,t,o)=>ap(e,typeof t!="symbol"?t+"":t,o),Ui=(e,t,o)=>t.has(e)||ba("Cannot "+o);var se=(e,t,o)=>(Ui(e,t,"read from private field"),o?o.call(e):t.get(e)),Ie=(e,t,o)=>t.has(e)?ba("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o),qe=(e,t,o,i)=>(Ui(e,t,"write to private field"),i?i.call(e,o):t.set(e,o),o),Me=(e,t,o)=>(Ui(e,t,"access private method"),o);var wa=(e,t,o,i)=>({set _(s){qe(e,t,s,o)},get _(){return se(e,t,i)}});var ge=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={markdown:"/script/markdown.6c8dfb7b1d.min.js",notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};function ka(e){let t=e.location.hash.slice(1);if(e.history.replaceState(null,"","/review-link"),t.length>2048)return null;let o=new URLSearchParams(t);if([...o.keys()].length!==3||[...o.keys()].some(a=>!["clientId","intentId","token"].includes(a)))return null;let i=o.get("clientId"),s=o.get("intentId"),r=o.get("token");return!/^[a-f0-9]{32}$/.test(i||"")||!/^[a-f0-9]{32}$/.test(s||"")||!/^[a-f0-9]{64}$/.test(r||"")?null:{contract:"4open.artifacts/1",clientId:i,intentId:s,token:r}}function Ca(e,t){let{http:o,promises:i,window:s}=t,r=t.takeReviewHandoff(),a=r?{clientId:r.clientId,intentId:r.intentId}:null,u,p,v,h,g,m=!0,k,T=i.defer(),w=()=>[...s.crypto.getRandomValues(new Uint8Array(16))].map(q=>q.toString(16).padStart(2,"0")).join("");Object.assign(e,{repositoryId:"",handoffEnabled:!1,busy:!1,preview:null,acceptAccess:!1,acceptRetention:!1,uncertain:!1,saved:!1,expired:!1,error:r?"":"Open a new artifact link from your submission. This link is missing or invalid."});let b=()=>m&&e.user?.username===g;function E(){m=!1,r=u=p=void 0,T.resolve(),s.clearTimeout(k)}e.on("dispose",E),e.on("routeLeave",E),e.watch(()=>e.user?.username,q=>{!g&&q?g=q:g&&q!==g&&(E(),e.preview=null,e.repositoryId="",e.busy=!1,e.saved=!1,e.handoffEnabled=!1,e.error="Your account changed. Open a new artifact link from your submission.")});let N=()=>({timeout:T.promise,headers:{"Content-Type":"application/json","X-Review-CSRF":p}}),O=q=>q?.status===404?"Artifact linking is not available on this server.":q?.status===401?"Sign in, then open a new artifact link from your submission.":q?.status===403?"This account cannot approve this repository or this link.":"The request could not be completed. You can retry from this page.";e.loadPreview=async()=>{if(!m||!r||e.busy||e.uncertain||e.saved||!e.user?.username)return;g=e.user.username;let q=e.repositoryId.trim();if(!/^[A-Za-z0-9_-]{3,128}$/.test(q)){e.error="Enter the anonymous repository ID from its URL.";return}e.busy=!0,e.error="",e.preview=null,u=void 0,e.acceptAccess=e.acceptRetention=!1,e.expired=!1,s.clearTimeout(k),h||(h=w());try{let M=(await o.get("/api/review-consent/csrf",{timeout:T.promise})).data;if(!b())return;if(p=M.csrf,e.handoffEnabled=M.handoffEnabled===!0,!/^[a-f0-9]{64}$/.test(p||""))throw new Error("invalid_csrf");let G=(await o.post("/api/review-consent/preview",{repositoryId:q,intent:{...r,requestId:h}},N())).data;if(!b())return;if(!G||typeof G.ticket!="string"||!G.ticket||G.ticket.length>8192||typeof G.repositoryName!="string"||!["restricted-review","anonymous-link"].iLine truncated
`))),e[rn]=ko(s);let r=i||s.props&&s.props.type==="number";Nn(e,t?"change":"input",a=>{a.target.composing||e[rn](Nr(e.value,o,r))}),(o||r)&&Nn(e,"change",()=>{e.value=Nr(e.value,o,r)}),t||(Nn(e,"compositionstart",ph),Nn(e,"compositionend",ld),Nn(e,"change",ld))},mounted(e,{value:t,modifiers:{trim:o,number:i}}){let s=t??"",r=e[pi];delete e[pi],r!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==r?e[rn](Nr(e.value,o,i)):e.value=s},beforeUpdate(e,{value:t,oldValue:o,modifiers:{lazy:i,trim:s,number:r}},a){if(e[rn]=ko(a),e.composing)return;let u=(r||e.type==="number")&&!/^0\d/.test(e.value)?fo(e.value):e.value,p=t??"";if(u===p)return;let v=e.getRootNode();(v instanceof Document||v instanceof ShadowRoot)&&v.activeElement===e&&e.type!=="range"&&(i&&t===o||s&&e.value.trim()===p)||(e.value=p)}},Dr={deep:!0,created(e,t,o){e[rn]=ko(o),Nn(e,"change",()=>{let i=e._modelValue,s=ms(e),r=e.checked,a=e[rn];if(Se(i)){let u=Fs(i,s),p=u!==-1;if(r&&!p)a(i.concat(s));else if(!r&&p){let v=[...i];v.splice(u,1),a(v)}}else if(Ut(i)){let u=new Set(i);r?u.add(s):u.delete(s),a(u)}else a(hd(e,r))})},mounted:dd,beforeUpdate(e,t,o){e[rn]=ko(o),dd(e,t,o)}};function dd(e,{value:t,oldValue:o},i){e._modelValue=t;let s;if(Se(t))s=Fs(t,i.props.value)>-1;else if(Ut(t))s=t.has(i.props.value);else{if(t===o)return;s=Ft(t,hd(e,!0))}e.checked!==s&&(e.checked=s)}var gn={deep:!0,created(e,{value:t,modifiers:{number:o}},i){e._modelValue=t,Nn(e,"change",()=>{let s=Array.prototype.filter.call(e.options,p=>p.selected).map(p=>o?fo(ms(p)):ms(p)),r=e.multiple,a=r?Ut(e._modelValue)?new Set(s):s:s[0],u=e._pendingValue=[r,r?Se(a)?s.slice():s:a];try{e[rn](a)}finally{nt(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[rn]=ko(i)},mounted(e,{value:t}){ud(e,t)},beforeUpdate(e,{value:t},o){e._modelValue=t,e[rn]=ko(o)},updated(e,{value:t}){let o=e._pendingValue;e._pendingValue=void 0,(!o||o[0]!==e.multiple||!fh(t,o[1],o[0]))&&ud(e,t)}};function fh(e,t,o){if(!o)return Ft(e,t);if(Se(e))return Ft(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let i of t)if(!e.has(i))return!1;return!0}return!1}function ud(e,t){let o=e.multiple,i=Se(t);if(!(o&&!i&&!Ut(t))){for(let s=0,r=e.options.length;s<r;s++){let a=e.options[s],u=ms(a);if(o)if(i){let p=typeof u;p==="string"||p==="number"?a.selected=t.some(v=>String(v)===String(u)):a.selected=Fs(t,u)>-1}else a.selected=t.has(u);else if(Ft(ms(a),t)){e.selectedIndex!==s&&(e.selectedIndex=s);return}}!o&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function ms(e){return"_value"in e?e._value:e.value}function hd(e,t){let o=t?"_trueValue":"_falseValue";return o in e?e[o]:t}var hh=["ctrl","shift","alt","meta"],mh={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>hh.some(o=>e[`${o}Key`]&&!t.includes(o))},ce=(e,t)=>{if(!e)return e;let o=e._withMods||(e._withMods={}),i=t.join(".");return o[i]||(o[i]=((s,...r)=>{for(let a=0;a<t.length;a++){let u=mh[t[a]];if(u&&u(s,t))return}return e(s,...r)}))},gh={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},At=(e,t)=>{let o=e._withKeys||(e._withKeys={}),i=t.join(".");return o[i]||(o[i]=(s=>{if(!("key"in s))return;let r=Xt(s.key);if(t.some(a=>a===r||gh[a]===r))return e(s)}))},vh=it({patchProp:dh},Kf),cd;function yh(){return cd||(cd=jl(vh))}var md=((...e)=>{let t=yh().createApp(...e),{mount:o}=t;return t.mount=i=>{let s=wh(i);if(!s)return;let r=t._component;!Te(r)&&!r.render&&!r.template&&(r.template=s.innerHTML),s.nodeType===1&&(s.textContent="");let a=o(s,!1,bh(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),a},t});function bh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function wh(e){return xe(e)?document.querySelector(e):e}var Xn=typeof document<"u";function vd(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function kh(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&vd(e.default)}var je=Object.assign;function hi(e,t){let o={};for(let i in t){let s=t[i];o[i]=Lt(s)?s.map(e):e(s)}return o}var Eo=()=>{},Lt=Array.isArray;function Pr(e,t){let o={};for(let i in e)o[i]=i in t?t[i]:e[i];return o}var yd=/#/g,Ch=/&/g,Eh=/\//g,Sh=/=/g,Nh=/\?/g,bd=/\+/g,Rh=/%5B/g,Dh=/%5D/g,wd=/%5E/g,Ah=/%60/g,kd=/%7B/g,Th=/%7C/g,Cd=/%7D/g,Ph=/%20/g;function _r(e){return e==null?"":encodeURI(""+e).replace(Th,"|").replace(Rh,"[").replace(Dh,"]")}function Ed(e){return _r(e).replace(kd,"{").replace(Cd,"}").replace(wd,"^")}function Ar(e){return _r(e).replace(bd,"%2B").replace(Ph,"+").replace(yd,"%23").replace(Ch,"%26").replace(Ah,"`").replace(kd,"{").replace(Cd,"}").replace(wd,"^")}function _h(e){return Ar(e).replace(Sh,"%3D")}function Oh(e){return _Line truncated
${JSON.stringify(e)}${t?`
while being at
`+JSON.stringify(t):""}`},[nt.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Uh(t)}" via a navigation guard.`},[nt.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[nt.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[nt.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function Yn(e,t){return je(new Error,{type:e,[Od]:!0},t)}function rn(e,t){return e instanceof Error&&Od in e&&(t==null||!!(e.type&t))}var Lh=["params","query","hash"];function Uh(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let s of Lh)s in e&&(t[s]=e[s]);return JSON.stringify(t,null,2)}function Pd(e){let t={};if(e===""||e==="?")return t;let s=(e[0]==="?"?e.slice(1):e).split("&");for(let i=0;i<s.length;++i){let o=s[i].replace(yd," "),r=o.indexOf("="),a=ws(r<0?o:o.slice(0,r)),u=r<0?null:ws(o.slice(r+1));if(a in t){let c=t[a];Lt(c)||(c=t[a]=[c]),c.push(u)}else t[a]=u}return t}function Mr(e){let t="";for(let s in e){let i=e[s];if(s=Ah(s),i==null){i!==void 0&&(t+=(t.length?"&":"")+s);continue}(Lt(i)?i.map(o=>o&&Rr(o)):[i&&Rr(i)]).forEach(o=>{o!==void 0&&(t+=(t.length?"&":"")+s,o!=null&&(t+="="+o))})}return t}function Id(e){let t={};for(let s in e){let i=e[s];i!==void 0&&(t[s]=Lt(i)?i.map(o=>o==null?null:""+o):i==null?i:""+i)}return t}var Md=Symbol(""),Vr=Symbol(""),yi=Symbol(""),bi=Symbol(""),wi=Symbol("");function Cs(){let e=[];function t(i){return e.push(i),()=>{let o=e.indexOf(i);o>-1&&e.splice(o,1)}}function s(){e=[]}return{add:t,list:()=>e.slice(),reset:s}}function mn(e,t,s,i,o,r=a=>a()){let a=i&&(i.enterCallbacks[o]=i.enterCallbacks[o]||[]);return()=>new Promise((u,c)=>{let m=h=>{h===!1?c(Yn(nt.NAVIGATION_ABORTED,{from:s,to:t})):h instanceof Error?c(h):$h(h)?c(Yn(nt.NAVIGATION_GUARD_REDIRECT,{from:t,to:h})):(a&&i.enterCallbacks[o]===a&&typeof h=="function"&&a.push(h),u())},g=r(()=>e.call(i&&i.instances[o],t,s,m)),v=Promise.resolve(g);e.length<3&&(v=v.then(m)),v.catch(h=>c(h))})}function ki(e,t,s,i,o=r=>r()){let r=[];for(let a of e)for(let u in a.components){let c=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(gd(c)){let m=(c.__vccOpts||c)[t];m&&r.push(mn(m,s,i,a,u,o))}else{let m=c();r.push(()=>m.then(g=>{if(!g)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let v=bh(g)?g.default:g;a.mods[u]=g,a.components[u]=v;let h=(v.__vccOpts||v)[t];return h&&mn(h,s,i,a,u,o)()}))}}return r}function Vd(e,t){let s=[],i=[],o=[],r=Math.max(t.matched.length,e.matched.length);for(let a=0;a<r;a++){let u=t.matched[a];u&&(e.matched.find(m=>Kn(m,u))?i.push(u):s.push(u));let c=e.matched[a];c&&(t.matched.find(m=>Kn(m,c))||o.push(c))}return[s,i,o]}var Fh=()=>location.protocol+"//"+location.host;function Wd(e,t){let{pathname:s,search:i,hash:o}=t,r=e.indexOf("#");if(r>-1){let a=o.includes(e.slice(r))?e.slice(r).length:1,u=o.slice(a);return u[0]!=="/"&&(u="/"+u),_r(u,"")}return _r(s,e)+i+o}function Bh(e,t,s,i){let o=[],r=[],a=null,u=({state:h})=>{let k=Wd(e,location),A=s.value,w=t.value,b=0;if(h){if(s.value=k,t.value=h,a&&a===A){a=null;return}b=w?h.position-w.position:0}else i(k);o.forEach(E=>{E(s.value,A,{delta:b,type:gi.pop,direction:b?b>0?vi.forward:vi.back:vi.unknown})})};function c(){a=s.value}function m(h){o.push(h);let k=()=>{let A=o.indexOf(h);A>-1&&o.splice(A,1)};return r.push(k),k}function g(){if(document.visibilityState==="hidden"){let{history:h}=window;if(!h.state)return;h.replaceState(je({},h.state,{scroll:mo()}),"")}}function v(){for(let h of r)h();r=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",g),document.removeEventListener("visibilitychange",g)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",g),document.addEventListener("visibilitychange",g),{pauseListeners:c,listen:m,destroy:v}}function qd(e,t,s,i=!1,o=!1){return{back:e,current:t,forward:s,replaced:i,position:window.history.length,scroll:o?mo():null}}function zh(e){let{history:t,location:s}=window,i={value:Wd(e,s)},o={value:t.state};o.value||r(i.value,{back:null,current:i.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function r(c,m,g){let v=e.indexOf("#"),h=v>-1?(s.host&&document.querySelector("base")?e:e.slice(v))+c:Fh()+e+c;try{t[g?"replaceState":"pushState"](m,"",h),o.value=m}catch(k){console.error(k),s[g?"replace":"assign"](h)}}function a(c,m){r(c,je({},t.state,qd(o.value.back,c,o.value.forward,!0),m,{position:o.value.position}),!0),i.value=c}function u(c,m){let g=je({},o.value,t.state,{forward:c,scroll:mo()});r(g.current,g,!0),r(c,je({},qd(i.value,c,null),{position:g.position+1},m),!1),i.value=c}return{location:i,state:o,push:u,replace:a}}function Kd(e){e=Rd(e);let t=zh(e),s=Bh(e,t.state,t.location,t.replace);function i(r,a=!0){a||s.pauseListeners(),history.go(r)}let o=je({locatLine truncated
`)},Kr=class{constructor(t){We(this,"gen");We(this,"threads",[]);We(this,"match",!1);We(this,"runs",[]);We(this,"ids",[]);this.gen=t}get dead(){return!this.threads.length&&!this.match&&!this.runs.length}},fu=(e,t)=>e.ids.length!==t.ids.length||e.runs.length!==t.runs.length||e.ids.some((s,i)=>s!==t.ids[i])?!1:e.runs.every(s=>t.runs.some(i=>i.neg===s.neg&&fu(i.state,s.state))),ru=e=>({has:new Float64Array(e),seen:new Float64Array(e),gen:0}),Zn,_n,Yr,Xr,bo=class{constructor(t,s,i){Ie(this,_n);We(this,"dots");We(this,"dotStart");Ie(this,Zn);this.dots=t,this.dotStart=s,qe(this,Zn,i)}state(){return new Kr(++oe(this,Zn).gen)}done(t){return t.ids=t.threads.map(s=>s.id).sort((s,i)=>s-i),t.match&&t.ids.unshift(Wr.id),t}restricted(t,s){return s&&(this.dots||this.dotStart&&t===0)}add(t,s,i,o){switch(s.t){case"split":if(!Me(this,_n,Yr).call(this,t,s))return;for(let a of s.out)this.add(t,a,i,o);return;case"neg":if(!Me(this,_n,Yr).call(this,t,s))return;let r=this.state();return this.add(r,s.sub,i,!1),this.done(r),this.addRun(t,{t:"run",neg:s,state:r},i,o);case"star":if(this.restricted(i,o))return;Me(this,_n,Xr).call(this,t,s)&&this.add(t,s.next,i,o);return;case"match":t.match=!0;return;default:Me(this,_n,Xr).call(this,t,s)}}addRun(t,s,i,o){for(let r of t.runs)if(r.neg===s.neg&&fu(r.state,s.state))return;t.runs.push(s),s.state.match||this.add(t,s.neg.next,i,o)}step(t,s,i,o){let r=this.state();for(let a of t.threads)switch(a.t){case"lit":Zr(a,s)&&this.add(r,a.next,i+1,o);break;case"any":this.restricted(i,o)||this.add(r,a.next,i+1,o);break;case"class":!this.restricted(i,o)&&a.re.test(s)&&this.add(r,a.next,i+1,o);break;case"star":this.restricted(i,o)||this.add(r,a,i+1,o)}if(!this.restricted(i,o))for(let a of t.runs){if(a.state.dead){this.addRun(r,a,i+1,o);continue}let u=this.step(a.state,s,i,!1);this.addRun(r,{t:"run",neg:a.neg,state:u},i+1,o)}return this.done(r)}};Zn=new WeakMap,_n=new WeakSet,Yr=function(t,s){let{seen:i}=oe(this,Zn);return i[s.id]===t.gen?!1:(i[s.id]=t.gen,!0)},Xr=function(t,s){let{has:i}=oe(this,Zn);return i[s.id]===t.gen?!1:(i[s.id]=t.gen,t.threads.push(s),!0)};var hu=e=>{let t=e.ids.join();if(!e.runs.length)return t;let s=e.runs.map(i=>`${i.neg.id}(${hu(i.state)})`);return`${t}|${s.sort().join("|")}`},Um=1<<18,Es,Ss,Nn,xn,Rn,Dn,Tn,Kt,Qr,Ri,mu,Jr=class{constructor(t,s){Ie(this,Kt);Ie(this,Es);Ie(this,Ss,new Map);Ie(this,Nn,0);Ie(this,xn);Ie(this,Rn);Ie(this,Dn);Ie(this,Tn);qe(this,Es,t),qe(this,Dn,new bo(!1,!1,s)),qe(this,Tn,new bo(!1,!0,s))}start(t){if(t){if(oe(this,Rn))return oe(this,Rn);let i=oe(this,Tn).state();return oe(this,Tn).add(i,oe(this,Es),0,!0),qe(this,Rn,Me(this,Kt,Qr).call(this,oe(this,Tn).done(i)))}if(oe(this,xn))return oe(this,xn);let s=oe(this,Dn).state();return oe(this,Dn).add(s,oe(this,Es),0,!0),qe(this,xn,Me(this,Kt,Ri).call(this,oe(this,Dn).done(s)))}next(t,s){let i=s.charCodeAt(0),o=i<128?void 0:t.next.get(s);if(o)return o;let r=t===oe(this,Rn)?Me(this,Kt,Ri).call(this,oe(this,Tn).step(t.state,s,0,!0)):Me(this,Kt,Ri).call(this,oe(this,Dn).step(t.state,s,1,!0));return i<128?(t.ascii||(t.ascii=new Array(128),qe(this,Nn,oe(this,Nn)+128)),t.ascii[i]=r):t.next.set(s,r),ba(this,Nn)._++,r}};Es=new WeakMap,Ss=new WeakMap,Nn=new WeakMap,xn=new WeakMap,Rn=new WeakMap,Dn=new WeakMap,Tn=new WeakMap,Kt=new WeakSet,Qr=function(t){return{state:t,match:t.match,dead:t.dead,next:new Map}},Ri=function(t){let s=hu(t),i=oe(this,Ss).get(s);if(i)return i;oe(this,Nn)>Um&&Me(this,Kt,mu).call(this);let o=Me(this,Kt,Qr).call(this,t);return oe(this,Ss).set(s,o),qe(this,Nn,oe(this,Nn)+(s.length+1)),o},mu=function(){oe(this,Ss).clear(),qe(this,Nn,0),qe(this,xn,void 0),qe(this,Rn,void 0)};var Fm=e=>{let t=[];for(let s=e;s.t!=="match";s=s.next){if(s.t==="split"||s.t==="neg")return;t.push(s)}return t},Bm=(e,t,s,i)=>{let o=0,r=s,a=-1,u=0;for(;r<i;){let c=e[o];if(c?.t==="star"){a=o++,u=r;continue}let m=t[r];if(c&&(c.t==="any"||c.t==="lit"&&Zr(c,m)||c.t==="class"&&c.re.test(m))){o++,r++;continue}if(a===-1)return!1;o=a+1,r=++u}for(;e[o]?.t==="star";)o++;return o===e.length},gu=e=>{let t=0,s=0;for(let i of e){let[o,r]=zm(i);t+=o,s+=r}return[t,s]},zm=e=>{if(e.t==="star")return[0,1/0];if(e.t!=="ext")return[1,1];if(e.type==="!")return[0,1/0];let t=e.alts.map(gu),s=Math.min(...t.map(r=>r[0])),i=Math.max(...t.map(r=>r[1])),o=i===0?0:1/0;return e.type==="@"?[s,i]:e.type==="?"?[0,i]:e.type==="*"?[0,o]:[s,o]},au=(e,t,s)=>{for(let i=0;i<e.length;i++)if(!Zr(e[i],t[s+i]))return!1;return!0},Hm=/[/\ud800-\udfff]/,jm=/[\ud800-\udfff]/,Ni=e=>jm.test(e)?[...e]:e,lu=e=>{if(e.includes("/"))throw new Error("can only match a single path portion, got: "+e)},An,Ns,es,Di,ts,ko,Ti,ns,Co,Eo,So,yn,an,Rs,Ds,No,Ai,Wt,ss,yo,wo=class{constructor(t,s={}){Ie(this,ss);We(this,"pattern");We(this,"options");Ie(this,An);Ie(this,Ns);Ie(this,es);Ie(this,Di);Ie(this,ts);Ie(this,ko,!1);Ie(this,Ti);Ie(this,ns);Ie(this,Co,!1);Ie(this,Eo,0);Ie(this,So,1/0);Ie(this,yn,[]);Ie(this,an,[]);Ie(this,Rs);Ie(this,Ds);Ie(this,No,!1);Ie(this,Ai);Ie(this,Wt);if(t.includes Line truncated
`).forEach((s,i)=>{let{term:o}=ua(s.trim());if(!o)return;let r=[];for(let a=0;a<o.length;a++){if(o[a]==="\\"){a++;continue}Iu.has(o[a])&&!r.includes(o[a])&&r.push(o[a])}r.length&&t.push({line:i,term:o,symbols:r})}),t}function Vu(e,t){let s=String(e||"").split(`
`),i=s[t];if(i===void 0)return e;let{term:o}=ua(i.trim());if(!o)return e;let r="";for(let u=0;u<o.length;u++){if(o[u]==="\\"){r+=o.slice(u,u+2),u++;continue}r+=Iu.has(o[u])||o[u]==="."?"\\"+o[u]:o[u]}let a=i.indexOf(o);return s[t]=i.slice(0,a)+r+i.slice(a+o.length),s.join(`
`)}var Oo={VIEWER_ERRORS:{title:"Content could not be loaded",access_title:"GitHub access needs attention",reader_help:"Reading this repository? You do not need to sign in. Try again after the owner restores access, or let them know this anonymous link is unavailable.",owner_help:"Own this repository? If your GitHub account was suspended or recently recovered, sign in to Anonymous GitHub again and check the GitHub connection and repository permissions in your dashboard. Then retry this page. Keep the existing anonymous URL; do not delete the repository to restore access.",retry:"Try again",dashboard:"Open owner dashboard"},ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"We could not load this content. Try again. If it still fails, contact support with the anonymous URL and the time of the error.",unreachable:"The service could not complete this request. Wait a moment and try again.",request_error:"The connection was interrupted. Check your internet connection and try again.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"Anonymous GitHub cannot access the source repository on GitHub. It may be private, unavailable, or require the owner to reconnect GitHub.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub temporarily blocked the request because we hit its API rate limit. Wait a few minutes and try again. If the problem persists and you contact GitHub Support, include the request ID and timestamp shown in GitHub's response (the 'X-GitHub-Request-Id' header).",rate_limited:"GitHub API rate limit reached. The repository will be available shortly \u2014 please wait a moment and refresh.",repoId_already_used:"The repository ID is already used.",invalid_repoId:"The format of the repository ID is invalid.",unsupported_source:"The repository source type is not supported.",branch_not_specified:"The branch is not specified.",branch_not_found:"The branch of the repository cannot be found.",commit_not_specified:"A commit must be specified.",commit_not_found:"The configured commit no longer exists in the source repository. It may have been force-pushed, rebased, or removed.",repo_renamed:"The source repository appears to have been renamed or moved on GitHub. Please refresh and try again \u2014 the cached name has been updated.",invalid_commit_format:"The commit hash format is invalid. It must be a hexadecimal string.",pull_request_not_found:"The requested pull request could not be found.",pull_request_expired:"This pull request has expired and is no longer available.",pull_request_not_ready:"This pull request is still being prepared. Please t Line truncated
`)),e.options=Object.assign({},e.options,g.options),a.restore()})}u();let c=null;e.saveDefault=m=>{if(m&&m.preventDefault&&m.preventDefault(),e.saving)return;let g=a.checkpoint(),v={terms:e.terms.split(`
`).map(h=>h.trim()).filter(h=>h.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",v).then(()=>{a.accept(g),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",c&&i.cancel(c),c=i(()=>{e.message=null},2500)},h=>{e.saving=!1;let k=h&&h.data&&h.data.error,A="Unable to save your defaults. Please try again.";Promise.all([s("ERRORS."+k),s("ERRORS.unknown_error")]).then(([w,b])=>{e.error=k&&w!==b?w:A},()=>{e.error=A})})},e.deleteAccount=()=>{e.deletingAccount||!e.confirmDeleteAccount||e.deleteUsername!==e.user?.username||(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},ju=function(e,t,s){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(i=>{s.url("/dashboard")},i=>{e.error=i.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Gu=function(e,t,s,i,o){e.user&&!e.user.status&&s.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&s.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,c){let m={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(m===void 0)return;u.preventDefault();let g=e.features.length,v=m==="first"?0:m==="last"?g-1:(c+m+g)%g;e.feature=e.features[v].key,o(()=>{let h=i.document.getElementById("feature-tab-"+e.feature);h&&h.focus()})},e.demoText=`# DeepLearnUtils
`+JSON.stringify(t):""}`},[ot.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Bh(t)}" via a navigation guard.`},[ot.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[ot.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[ot.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function Yn(e,t){return je(new Error,{type:e,[Od]:!0},t)}function an(e,t){return e instanceof Error&&Od in e&&(t==null||!!(e.type&t))}var Fh=["params","query","hash"];function Bh(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of Fh)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function Id(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let i=0;i<o.length;++i){let s=o[i].replace(bd," "),r=s.indexOf("="),a=Co(r<0?s:s.slice(0,r)),u=r<0?null:Co(s.slice(r+1));if(a in t){let p=t[a];Lt(p)||(p=t[a]=[p]),p.push(u)}else t[a]=u}return t}function qr(e){let t="";for(let o in e){let i=e[o];if(o=_h(o),i==null){i!==void 0&&(t+=(t.length?"&":"")+o);continue}(Lt(i)?i.map(s=>s&&Ar(s)):[i&&Ar(i)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+o,s!=null&&(t+="="+s))})}return t}function Md(e){let t={};for(let o in e){let i=e[o];i!==void 0&&(t[o]=Lt(i)?i.map(s=>s==null?null:""+s):i==null?i:""+i)}return t}var Vd=Symbol(""),$r=Symbol(""),yi=Symbol(""),bi=Symbol(""),wi=Symbol("");function So(){let e=[];function t(i){return e.push(i),()=>{let s=e.indexOf(i);s>-1&&e.splice(s,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function vn(e,t,o,i,s,r=a=>a()){let a=i&&(i.enterCallbacks[s]=i.enterCallbacks[s]||[]);return()=>new Promise((u,p)=>{let v=m=>{m===!1?p(Yn(ot.NAVIGATION_ABORTED,{from:o,to:t})):m instanceof Error?p(m):Uh(m)?p(Yn(ot.NAVIGATION_GUARD_REDIRECT,{from:t,to:m})):(a&&i.enterCallbacks[s]===a&&typeof m=="function"&&a.push(m),u())},h=r(()=>e.call(i&&i.instances[s],t,o,v)),g=Promise.resolve(h);e.length<3&&(g=g.then(v)),g.catch(m=>p(m))})}function ki(e,t,o,i,s=r=>r()){let r=[];for(let a of e)for(let u in a.components){let p=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(vd(p)){let v=(p.__vccOpts||p)[t];v&&r.push(vn(v,o,i,a,u,s))}else{let v=p();r.push(()=>v.then(h=>{if(!h)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let g=kh(h)?h.default:h;a.mods[u]=h,a.components[u]=g;let m=(g.__vccOpts||g)[t];return m&&vn(m,o,i,a,u,s)()}))}}return r}function qd(e,t){let o=[],i=[],s=[],r=Math.max(t.matched.length,e.matched.length);for(let a=0;a<r;a++){let u=t.matched[a];u&&(e.matched.find(v=>Kn(v,u))?i.push(u):o.push(u));let p=e.matched[a];p&&(t.matched.find(v=>Kn(v,p))||s.push(p))}return[o,i,s]}var zh=()=>location.protocol+"//"+location.host;function Kd(e,t){let{pathname:o,search:i,hash:s}=t,r=e.indexOf("#");if(r>-1){let a=s.includes(e.slice(r))?e.slice(r).length:1,u=s.slice(a);return u[0]!=="/"&&(u="/"+u),Or(u,"")}return Or(o,e)+i+s}function Hh(e,t,o,i){let s=[],r=[],a=null,u=({state:m})=>{let k=Kd(e,location),T=o.value,w=t.value,b=0;if(m){if(o.value=k,t.value=m,a&&a===T){a=null;return}b=w?m.position-w.position:0}else i(k);s.forEach(E=>{E(o.value,T,{delta:b,type:gi.pop,direction:b?b>0?vi.forward:vi.back:vi.unknown})})};function p(){a=o.value}function v(m){s.push(m);let k=()=>{let T=s.indexOf(m);T>-1&&s.splice(T,1)};return r.push(k),k}function h(){if(document.visibilityState==="hidden"){let{history:m}=window;if(!m.state)return;m.replaceState(je({},m.state,{scroll:gs()}),"")}}function g(){for(let m of r)m();r=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",h),document.removeEventListener("visibilitychange",h)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",h),document.addEventListener("visibilitychange",h),{pauseListeners:p,listen:v,destroy:g}}function $d(e,t,o,i=!1,s=!1){return{back:e,current:t,forward:o,replaced:i,position:window.history.length,scroll:s?gs():null}}function jh(e){let{history:t,location:o}=window,i={value:Kd(e,o)},s={value:t.state};s.value||r(i.value,{back:null,current:i.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function r(p,v,h){let g=e.indexOf("#"),m=g>-1?(o.host&&document.querySelector("base")?e:e.slice(g))+p:zh()+e+p;try{t[h?"replaceState":"pushState"](v,"",m),s.value=v}catch(k){console.error(k),o[h?"replace":"assign"](m)}}function a(p,v){r(p,je({},t.state,$d(s.value.back,p,s.value.forward,!0),v,{position:s.value.position}),!0),i.value=p}function u(p,v){let h=je({},s.value,t.state,{forward:p,scroll:gs()});r(h.current,h,!0),r(p,je({},$d(i.value,p,null),{position:h.position+1},v),!1),i.value=p}return{location:i,state:s,push:u,replace:a}}function Yd(e){e=Dd(e);let t=jh(e),o=Hh(e,t.state,t.location,t.replace);function i(r,a=!0){a||o.pauseListeners(),history.go(r)}let s=je({locatLine truncated
`)},Yr=class{constructor(t){We(this,"gen");We(this,"threads",[]);We(this,"match",!1);We(this,"runs",[]);We(this,"ids",[]);this.gen=t}get dead(){return!this.threads.length&&!this.match&&!this.runs.length}},hu=(e,t)=>e.ids.length!==t.ids.length||e.runs.length!==t.runs.length||e.ids.some((o,i)=>o!==t.ids[i])?!1:e.runs.every(o=>t.runs.some(i=>i.neg===o.neg&&hu(i.state,o.state))),au=e=>({has:new Float64Array(e),seen:new Float64Array(e),gen:0}),Zn,_n,Xr,Jr,ws=class{constructor(t,o,i){Ie(this,_n);We(this,"dots");We(this,"dotStart");Ie(this,Zn);this.dots=t,this.dotStart=o,qe(this,Zn,i)}state(){return new Yr(++se(this,Zn).gen)}done(t){return t.ids=t.threads.map(o=>o.id).sort((o,i)=>o-i),t.match&&t.ids.unshift(Kr.id),t}restricted(t,o){return o&&(this.dots||this.dotStart&&t===0)}add(t,o,i,s){switch(o.t){case"split":if(!Me(this,_n,Xr).call(this,t,o))return;for(let a of o.out)this.add(t,a,i,s);return;case"neg":if(!Me(this,_n,Xr).call(this,t,o))return;let r=this.state();return this.add(r,o.sub,i,!1),this.done(r),this.addRun(t,{t:"run",neg:o,state:r},i,s);case"star":if(this.restricted(i,s))return;Me(this,_n,Jr).call(this,t,o)&&this.add(t,o.next,i,s);return;case"match":t.match=!0;return;default:Me(this,_n,Jr).call(this,t,o)}}addRun(t,o,i,s){for(let r of t.runs)if(r.neg===o.neg&&hu(r.state,o.state))return;t.runs.push(o),o.state.match||this.add(t,o.neg.next,i,s)}step(t,o,i,s){let r=this.state();for(let a of t.threads)switch(a.t){case"lit":xr(a,o)&&this.add(r,a.next,i+1,s);break;case"any":this.restricted(i,s)||this.add(r,a.next,i+1,s);break;case"class":!this.restricted(i,s)&&a.re.test(o)&&this.add(r,a.next,i+1,s);break;case"star":this.restricted(i,s)||this.add(r,a,i+1,s)}if(!this.restricted(i,s))for(let a of t.runs){if(a.state.dead){this.addRun(r,a,i+1,s);continue}let u=this.step(a.state,o,i,!1);this.addRun(r,{t:"run",neg:a.neg,state:u},i+1,s)}return this.done(r)}};Zn=new WeakMap,_n=new WeakSet,Xr=function(t,o){let{seen:i}=se(this,Zn);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,!0)},Jr=function(t,o){let{has:i}=se(this,Zn);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,t.threads.push(o),!0)};var mu=e=>{let t=e.ids.join();if(!e.runs.length)return t;let o=e.runs.map(i=>`${i.neg.id}(${mu(i.state)})`);return`${t}|${o.sort().join("|")}`},Bm=1<<18,No,Ro,Rn,xn,Dn,An,Tn,Kt,Zr,Ai,gu,Qr=class{constructor(t,o){Ie(this,Kt);Ie(this,No);Ie(this,Ro,new Map);Ie(this,Rn,0);Ie(this,xn);Ie(this,Dn);Ie(this,An);Ie(this,Tn);qe(this,No,t),qe(this,An,new ws(!1,!1,o)),qe(this,Tn,new ws(!1,!0,o))}start(t){if(t){if(se(this,Dn))return se(this,Dn);let i=se(this,Tn).state();return se(this,Tn).add(i,se(this,No),0,!0),qe(this,Dn,Me(this,Kt,Zr).call(this,se(this,Tn).done(i)))}if(se(this,xn))return se(this,xn);let o=se(this,An).state();return se(this,An).add(o,se(this,No),0,!0),qe(this,xn,Me(this,Kt,Ai).call(this,se(this,An).done(o)))}next(t,o){let i=o.charCodeAt(0),s=i<128?void 0:t.next.get(o);if(s)return s;let r=t===se(this,Dn)?Me(this,Kt,Ai).call(this,se(this,Tn).step(t.state,o,0,!0)):Me(this,Kt,Ai).call(this,se(this,An).step(t.state,o,1,!0));return i<128?(t.ascii||(t.ascii=new Array(128),qe(this,Rn,se(this,Rn)+128)),t.ascii[i]=r):t.next.set(o,r),wa(this,Rn)._++,r}};No=new WeakMap,Ro=new WeakMap,Rn=new WeakMap,xn=new WeakMap,Dn=new WeakMap,An=new WeakMap,Tn=new WeakMap,Kt=new WeakSet,Zr=function(t){return{state:t,match:t.match,dead:t.dead,next:new Map}},Ai=function(t){let o=mu(t),i=se(this,Ro).get(o);if(i)return i;se(this,Rn)>Bm&&Me(this,Kt,gu).call(this);let s=Me(this,Kt,Zr).call(this,t);return se(this,Ro).set(o,s),qe(this,Rn,se(this,Rn)+(o.length+1)),s},gu=function(){se(this,Ro).clear(),qe(this,Rn,0),qe(this,xn,void 0),qe(this,Dn,void 0)};var zm=e=>{let t=[];for(let o=e;o.t!=="match";o=o.next){if(o.t==="split"||o.t==="neg")return;t.push(o)}return t},Hm=(e,t,o,i)=>{let s=0,r=o,a=-1,u=0;for(;r<i;){let p=e[s];if(p?.t==="star"){a=s++,u=r;continue}let v=t[r];if(p&&(p.t==="any"||p.t==="lit"&&xr(p,v)||p.t==="class"&&p.re.test(v))){s++,r++;continue}if(a===-1)return!1;s=a+1,r=++u}for(;e[s]?.t==="star";)s++;return s===e.length},vu=e=>{let t=0,o=0;for(let i of e){let[s,r]=jm(i);t+=s,o+=r}return[t,o]},jm=e=>{if(e.t==="star")return[0,1/0];if(e.t!=="ext")return[1,1];if(e.type==="!")return[0,1/0];let t=e.alts.map(vu),o=Math.min(...t.map(r=>r[0])),i=Math.max(...t.map(r=>r[1])),s=i===0?0:1/0;return e.type==="@"?[o,i]:e.type==="?"?[0,i]:e.type==="*"?[0,s]:[o,s]},lu=(e,t,o)=>{for(let i=0;i<e.length;i++)if(!xr(e[i],t[o+i]))return!1;return!0},Gm=/[/\ud800-\udfff]/,Wm=/[\ud800-\udfff]/,Di=e=>Wm.test(e)?[...e]:e,du=e=>{if(e.includes("/"))throw new Error("can only match a single path portion, got: "+e)},Pn,Do,eo,Ti,to,Cs,Pi,no,Es,Ss,Ns,wn,ln,Ao,To,Rs,_i,Wt,oo,bs,ks=class{constructor(t,o={}){Ie(this,oo);We(this,"pattern");We(this,"options");Ie(this,Pn);Ie(this,Do);Ie(this,eo);Ie(this,Ti);Ie(this,to);Ie(this,Cs,!1);Ie(this,Pi);Ie(this,no);Ie(this,Es,!1);Ie(this,Ss,0);Ie(this,Ns,1/0);Ie(this,wn,[]);Ie(this,ln,[]);Ie(this,Ao);Ie(this,To);Ie(this,Rs,!1);Ie(this,_i);Ie(this,Wt);if(t.includes Line truncated
`).forEach((o,i)=>{let{term:s}=ca(o.trim());if(!s)return;let r=[];for(let a=0;a<s.length;a++){if(s[a]==="\\"){a++;continue}Mu.has(s[a])&&!r.includes(s[a])&&r.push(s[a])}r.length&&t.push({line:i,term:s,symbols:r})}),t}function qu(e,t){let o=String(e||"").split(`
`),i=o[t];if(i===void 0)return e;let{term:s}=ca(i.trim());if(!s)return e;let r="";for(let u=0;u<s.length;u++){if(s[u]==="\\"){r+=s.slice(u,u+2),u++;continue}r+=Mu.has(s[u])||s[u]==="."?"\\"+s[u]:s[u]}let a=i.indexOf(s);return o[t]=i.slice(0,a)+r+i.slice(a+s.length),o.join(`
`)}var ro={VIEWER_ERRORS:{title:"Content could not be loaded",access_title:"GitHub access needs attention",reader_help:"Reading this repository? You do not need to sign in. Try again after the owner restores access, or let them know this anonymous link is unavailable.",owner_help:"Own this repository? If your GitHub account was suspended or recently recovered, sign in to Anonymous GitHub again and check the GitHub connection and repository permissions in your dashboard. Then retry this page. Keep the existing anonymous URL; do not delete the repository to restore access.",retry:"Try again",dashboard:"Open owner dashboard"},ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"We could not load this content. Try again. If it still fails, contact support with the anonymous URL and the time of the error.",unreachable:"The service could not complete this request. Wait a moment and try again.",request_error:"The connection was interrupted. Check your internet connection and try again.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"Anonymous GitHub cannot access the source repository on GitHub. It may be private, unavailable, or require the owner to reconnect GitHub.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub temporarily blocked the request because we hit its API rate limit. Wait a few minutes and try again. If the problem persists and you contact GitHub Support, include the request ID and timestamp shown in GitHub's response (the 'X-GitHub-Request-Id' header).",rate_limited:"GitHub API rate limit reached. The repository will be available shortly \u2014 please wait a moment and refresh.",repoId_already_used:"The repository ID is already used.",invalid_repoId:"The format of the repository ID is invalid.",unsupported_source:"The repository source type is not supported.",branch_not_specified:"The branch is not specified.",branch_not_found:"The branch of the repository cannot be found.",commit_not_specified:"A commit must be specified.",commit_not_found:"The configured commit no longer exists in the source repository. It may have been force-pushed, rebased, or removed.",repo_renamed:"The source repository appears to have been renamed or moved on GitHub. Please refresh and try again \u2014 the cached name has been updated.",invalid_commit_format:"The commit hash format is invalid. It must be a hexadecimal string.",pull_request_not_found:"The requested pull request could not be found.",pull_request_expired:"This pull request has expired and is no longer available.",pull_request_not_ready:"This pull request is still being prepared. Please t Line truncated
`)),e.options=Object.assign({},e.options,g.options),u.restore()})}p();let v=null;e.saveDefault=h=>{if(h&&h.preventDefault&&h.preventDefault(),e.saving)return;let g=u.checkpoint(),m={terms:e.terms.split(`
`).map(k=>k.trim()).filter(k=>k.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",m).then(()=>{u.accept(g),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",v&&i.cancel(v),v=i(()=>{e.message=null},2500)},k=>{e.saving=!1;let T=k&&k.data&&k.data.error,w="Unable to save your defaults. Please try again.";Promise.all([o("ERRORS."+T),o("ERRORS.unknown_error")]).then(([b,E])=>{e.error=T&&b!==E?b:w},()=>{e.error=w})})},e.deleteAccount=()=>{e.deletingAccount||!e.confirmDeleteAccount||e.deleteUsername!==e.user?.username||(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Gu=function(e,t,o){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(i=>{o.url("/dashboard")},i=>{e.error=i.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Wu=function(e,t,o,i,s){e.user&&!e.user.status&&o.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&o.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,p){let v={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(v===void 0)return;u.preventDefault();let h=e.features.length,g=v==="first"?0:v==="last"?h-1:(p+v+h)%h;e.feature=e.features[g].key,s(()=>{let m=i.document.getElementById("feature-tab-"+e.feature);m&&m.focus()})},e.demoText=`# DeepLearnUtils
Developed by Jane Smith at MIT CSAIL.
See Smith et al., "Attention That Works," 2026.
Contact: jane.smith@mit.edu`,e.demoTerms="Jane Smith, Smith, MIT CSAIL, jane.smith@mit.edu",e.demoParts=function(){let u=(e.demoTerms||"").split(/[,\n]/).map(h=>h.trim()).filter(Boolean).sort((h,k)=>k.length-h.length),c=h=>h.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!u.length)return[{text:e.demoText}];let m=new RegExp("("+u.map(c).join("|")+")","gi"),g={},v=0;return(e.demoText||"").split(m).filter(h=>h!=="").map(h=>{let k=u.find(A=>A.toLowerCase()===h.toLowerCase());return k?(g[k.toLowerCase()]||(g[k.toLowerCase()]=++v),{text:String(g[k.toLowerCase()]),redacted:!0,original:h}):{text:h}})},e.demoCount=()=>e.demoParts().filter(u=>u.redacted).length,e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function r(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests}).catch(()=>{})}r();function a(u){let c={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return c;let m=new Array(u.length-1);for(let w=1;w<u.length;w++)m[w-1]=u[w]-u[w-1];let g=m.length,v=Math.max.apply(null,m),h=Math.min.apply(null,m),k=Math.min(0,h),A=v-k||1;c.viewW=g*2,c.bars=new Array(g);for(let w=0;w<g;w++){let b=(m[w]-k)/A,E=Math.max(1.5,b*34);c.bars[w]={x:(w*2+.25).toFixed(2),y:(36-E).toFixed(2),w:"1.5",h:E.toFixed(2)}}if(c.deltaToday=m[g-1],g>=2){let w=m[g-2];w&&(c.pctChange=(c.deltaToday-w)/w*100)}return c.pctAbs=Math.round(Math.abs(c.pctChange)),c.isUp=c.pctChange>=0,c}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let c=u.data||[];e.history={repositories:a(c.map(m=>m.nbRepositories||0)),users:a(c.map(m=>m.nbUsers||0)),pageViews:a(c.map(m=>m.nbPageViews||0)),pullRequests:a(c.map(m=>m.nbPullRequests||0))}}).catch(()=>{})},Wu=function(e,t,s,i,o,r){let a=At();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,it(()=>document.querySelector(".dashboard-quota > summary")?.focus())},vn()(document,"click",D=>{e.quotaExpanded&&!D.target.closest?.(".dashboard-quota")&&(e.quotaExpanded=!1)}),e.filtersExpanded=!1,e.projectView="all",e.selectedProject=null,e.projectName="",e.nameSaving=!1,e.nameError="",e.nameMessage="",e.needsAttention=D=>D.status==="error"||D._broken||c(D.status)==="progress"&&D._stale,e.attentionCount=()=>e.dashboardTotals?.attention??e.items.filter(e.needsAttention).length,e.hasProjects=()=>(e.dashboardTotals?.total??e.items.length)>0,e.setProjectView=D=>{e.projectView=D,D==="attention"&&(e.filters.status.error=!0,e.filters.status.progress=!0)},e.projectDetailsId=D=>"project-details-"+encodeURIComponent(D._type+":"+(D._id||D._source||"unknown")),e.showProjectDetails=async D=>{e.selectedProject=D,e.projectName=D.projectName||"",e.nameError="",e.nameMessage="",await it();let X=document.getElementById(e.projectDetailsId(D));X?.focus(),X?.scrollIntoView?.({block:"nearest"})},e.closeProjectDetails=D=>{e.selectedProject=null,it(()=>document.querySelector(`[data-project-actions="${encodeURIComponent(D._type+":"+D._id)}"]`)?.focus())},e.saveProjectName=async D=>{if(e.nameSaving||!D._id)return;let X=e.projectName.trim();if(X.length>100){e.nameError="Use 100 characters or fewer.";return}e.nameSaving=!0,e.nameError="",e.nameMessage="";try{let le=await t.post("/api/user/project-name",{type:D._type,id:D._id,name:X});D.projectName=le.data.name,D._label=D.projectName||D._fallbackLabel,e.selectedProject===D&&(e.nameMessage=X?"Project name saved.":"Default project name restored."),N||(e.dashboardCursor=null,await q(),e.selectedProject===D&&(e.selectedProject=e.items.find(Ee=>Ee._type===D._type&&Ee._id===D._id)||null))}catch{e.selectedProject===D&&(e.nameError="The project name could not be saved. Try again.")}finally{e.nameSaving=!1}},e.copyingId=null,e.copyLink=async D=>{if(!D._viewUrl||e.copyingId)return;let X=new URL(D._viewUrl,o.location.origin).href;e.copyingId=D._type+":"+D._id;try{await Fu(X),e.addToast({title:"Link copied",body:"The anonymous link is ready to share."})}catch{e.addToast({title:"Copy this anonymous link",body:X})}finally{e.copyingId=null}},e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"In progress",error:"Error",expired:"Expired",removed:"Removed"};let u=["queue","download","downloaded","preparing","anonymizing"];function c(D){return D==="ready"||D==="error"?D:D==="expired"||D==="expiring"?"expired":D==="removed"||D==="removing"?"removed":(u.indexOf(D)>-1,"progress")}let m=7200*1e3,g="dashboard.filterPrefs",v={typeFilter:"all",filters:{status:{ready:!0,progress:!0,error:!0,e Line truncated
`)),e.options=Object.assign({},e.options,z.options),e.options.expirationDate=z.options&&z.options.expirationDate?new Date(z.options.expirationDate):j(),!i.repoId&&!i.pullRequestId&&!i.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),S&&S()})}function re(S,V,z){e.anonymize&&e.anonymize[S]&&e.anonymize[S].setValidity(V,z)}function me(S){try{let V=parseGithubUrl(S);if(V&&V.owner&&V.repo)return V.owner+"/"+V.repo}catch{}return null}e.accessRepairAllowed=!1,e.repairingGitHubAccess=!1,e.accessRepairMessage="",e.repairGitHubAccess=async()=>{if(!e.accessRepairAllowed||e.repairingGitHubAccess||!e.githubConnections?.appEnabled||!i.repoId||e.role!=="owner"||e.githubConnection!=="github-app"||me(e.sourceUrl)!==e._originalFullName)return;e.repairingGitHubAccess=!0,e.accessRepairMessage="";let S=h();try{let V=await t.get("/github/connections");if(e.githubConnections=V.data,!V.data.appEnabled)return;let z=await t.post("/github/connections/migrate",{type:"repository",id:i.repoId,connection:"github-app"},{headers:{"X-CSRF-Token":V.data.csrf}});if(!v(S))return;e.error=null,z.data.fullName&&(e._originalFullName=z.data.fullName,e.source.fullName=z.data.fullName,e.sourceUrl="https://github.com/"+z.data.fullName),e.accessRepairMessage="GitHub access repaired. Your anonymous URL and published commit are unchanged.",await e.urlSelected(!0)}catch{v(S)&&(e.accessRepairMessage="Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced.")}finally{e.repairingGitHubAccess=!1,k(S)}},e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await Re()}catch{}}};function ke(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:me(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function _e(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function Ne(){let S=new URLSearchParams(window.location.search).get("billingDraft");if(S){b.restore();try{let z=(await t.get("/api/billing/draft/"+encodeURIComponent(S))).data;if(z.path!==o.path())throw new Error("Wrong task");E=z.values.conference||null;for(let Z of["sourceUrl","terms","repoId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(z.values,Z)&&(e[Z]=z.values[Z]);return e.options?.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),N=z.id,O=JSON.stringify(b.checkpoint()),e.draftRestored=!0,e.hostingReview=!0,e.sourceUrl&&await _e(),o.replace?.(o.path()),!0}catch{e.billingError="The checkout draft could not be restored. Retry by reloading this page; any local draft is still available."}}let V;try{V=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!V||V.path!==o.path()||Date.now()-V.savedAt>=30*6e4)return b.restore()?(e.sourceUrl&&await _e(),!0):!1;b.restore();for(let z of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(V.draft,z)&&(e[z]=V.draft[z]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await _e(),sessionStorage.removeItem("github-access-draft"),!0}Ee(()=>{!i.repoId&&!i.pullRequestId&&!i.gistId&&a(Ne,0),i.repoId&&i.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=i.repoId,t.get("/api/repo/"+e.repoId).then(async S=>{e.accessRepairAllowed=!["removed","archived","preparing","queue","download","removing","expiring"].includes(S.data.status),e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.fullName,e._originalFullName=S.data.source.fullName,e.terms=S.data.options.terms.filter(V=>V).join(`
`),e.source=S.data.source,e.role=S.data.role||"owner",e._existingNever=S.data.options.expirationMode==="never",e.coauthors=S.data.coauthors||[],e._originalBranch=S.data.source.branch,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,e.repositoryID=S.data.source.repositoryID,e._originalRepositoryID=S.data.source.repositoryID,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),A=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&(await Promise.all([Re(),Je()]),H())},()=>{o.url("/404")})),i.pullRequestId&&i.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=i.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async S=>{if(e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.repositoryFullName+"/pull/"+S.data.source.pullRequestId,e.terms=S.data.options.terms.filter(V=>V).join(`
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),A=w(parseGithubUrl(e.sourceUrl)),!await Ne())try{await ee()}catch{}},()=>{o.url("/404")})),i.gistId&&i.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=i.gistId,t.get("/api/gist/"+e.gistId).then(async S=>{e.sourceUrl="https://gist.github.com/"+S.data.source.gistId,e.terms=S.data.options.terms.filter(V=>V).join(`
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),A=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&await pe()},()=>{o.url("/404")}))}),t.get("/github/connections").then(S=>{e.githubConnections=S.data,!i.repoId&&!i.pullRequestId&&!i.gistId&&e.githubConnection===void 0&&S.data.appEnabled&&S.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&_e())}).catch(()=>{}),e.urlSelected=async(S=!1)=>{let V=++c;e.previousConfigurations=[],e.previousConfigurationId="";let z;try{z=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,M.cancel(),ne.cancel(),Le.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",re("sourceUrl","github",!1);return}let Z=w(z),fe=S||A===Z;A=Z,e.sourceUrlError="",re("sourceUrl","github",!0);let be=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,setTimeout(()=>{let Ze=window.document.getElementById("sourceUrl");if(Ze){try{Ze.setSelectionRange(0,0)}catch{}Ze.scrollLeft=0}},50),be&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!fe&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],fe||(e.source={type:"GitHubStream",branch:"",commit:""}),M.cancel(),ne.cancel(),Le.cancel(),K.clear(),ye.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=fe;try{z.gistId&&!z.repo?(e.detectedType="gist",e.source={gistId:z.gistId},await pe()):z.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:z.owner+"/"+z.repo,pullRequestId:z.pullRequestId},await ee()):(e.detectedType="repo",le(),await Promise.all([Re(),Je()]),H())}catch{}finally{V===c&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let S=e.branches.filter(z=>z.name==e.source.branch)[0];if(!S)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=S.commit),e.readme=S.readme,await Je(),H()}),e.getBranches=async S=>{let V=parseGithubUrl(e.sourceUrl),z=h();try{let Z=await t.get(`/api/repo/${V.owner}/${V.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&me(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0||e.reconnectingSource?"1":"0",repositoryID:ke()}});if(!v(z))return;e.branches=Z.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let fe=e.branches.filter(be=>be.name==e.source.branch);fe.length>0&&(!(!e.reconnectingSource&&!S&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=fe[0].commit),e.readme=fe[0].readme,await Je(S))}catch(Z){if(!v(z))return;e.branches=[],e.sourceUnreachable=Z&&(Z.status===404||Z.data&&Z.data.error==="repo_not_found");let fe=Z&&Z.data&&Z.data.error||(Z&&Z.status===404?"repo_not_found":"unknown_error");r("ERRORS."+fe).then(be=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:be}),e.error=be},console.error),typeof re=="function"&&re("sourceUrl","missing",!1)}finally{k(z)}};async function Re(){let S=parseGithubUrl(e.sourceUrl),V=h();try{Ye();let z=await t.get(`/api/repo/${S.owner}/${S.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&me(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,repositoryID:ke(),force:"1"}});if(!v(V))return;e.details=z.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(z){if(!v(V))return;throw z.data&&(r("ERRORS."+z.data.error).then(Z=>{e.addToast({title:"Error",date:new Date,body:Z}),e.error=Z},console.error),lt(z.data.error)),re("sourceUrl","missing",!1),z}finally{k(V)}}async function Je(S){if(e.readme&&!S)return e.readme;let V=parseGithubUrl(e.sourceUrl),z=h();try{let Z=await t.get(`/api/repo/${V.owner}/${V.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&me(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0?"1":"0",branch:e.source.branch,repositoryID:ke()}});v(z,!0)&&(e.readme=Z.data)}catch{v(z,!0)&&(e.readme="")}finally{k(z)}}let Ke={magicalBraces:!0},rt=S=>S.startsWith("/")&&!S.endsWith("/")&&On(os(S.slice(1),Ke),Ke)===S.slice(1),Qe=()=>Object.keys(e.preserve.picked).filter(S=>e. Line truncated
`).map(S=>S.trim()).filter(Boolean),te=null,L=0,x=null,B=()=>!1;function ue(){let S=L+"\0"+e.preserve.patterns;if(S!==x){x=S;try{B=da(la([...Qe(),...Q()]))}catch{B=()=>!1}}return B}function we(S){let V=new Map([["",{dirs:[],files:[]}]]);for(let z of S){let Z=z.split("/"),fe="";for(let be=0;be<Z.length-1;be++){let Ze=Z.slice(0,be+1).join("/");V.has(Ze)||(V.set(Ze,{dirs:[],files:[]}),V.get(fe).dirs.push(Ze)),fe=Ze}V.get(fe).files.push(z)}for(let z of V.values())z.dirs.sort();return{files:S,dirs:V,descendants:new Map}}function Pe(S){return te.descendants.has(S)||te.descendants.set(S,te.files.filter(V=>V.startsWith(S+"/"))),te.descendants.get(S)}let C=S=>ue()(S);function R(S){let V=Pe(S),z=0;for(let Z of V)C(Z)&&z++;return z===0?"none":z===V.length?"all":"some"}e.preserve={open:!1,loading:!1,error:"",truncated:!1,commit:"",search:"",expanded:{},picked:{},patterns:"",patternsOpen:!1},e.preserveSummary=()=>{let S=(e.options?.preserveFiles||[]).map(V=>String(V).trim()).filter(Boolean).map(V=>rt(V)?os(V.slice(1),Ke):V);return S.length?S.slice(0,2).join(", ")+(S.length>2?` and ${S.length-2} more`:""):"None. Every file is redacted."};async function P(){let S=e.source?.commit;if(!(te&&e.preserve.commit===S)){e.preserve.loading=!0,e.preserve.error="",te=null;try{let V=parseGithubUrl(e.sourceUrl),z=await t.get(`/api/repo/${V.owner}/${V.repo}/tree`,{params:{commit:S,connection:e.githubConnection,anonymizedRepoId:e.isUpdate&&i.repoId&&me(e.sourceUrl)===e._originalFullName?i.repoId:void 0,repositoryID:ke()}});if(e.source?.commit!==S)return;te=hs(we(z.data.files||[])),e.preserve.commit=S,e.preserve.truncated=!!z.data.truncated}catch{e.preserve.error="The file list could not be loaded from GitHub. You can still use patterns.",e.preserve.patternsOpen=!0}finally{e.preserve.loading=!1}}}e.openPreserveFiles=()=>{let S={},V=[];for(let Z of e.options?.preserveFiles||[]){let fe=String(Z).trim();fe&&(!V.length&&rt(fe)?S[os(fe.slice(1),Ke)]=!0:V.push(fe))}let z={};for(let Z of Object.keys(S)){let fe=Z.split("/");for(let be=1;be<fe.length;be++)z[fe.slice(0,be).join("/")]=!0}Object.assign(e.preserve,{open:!0,search:"",picked:S,expanded:z,patterns:V.join(`
`),patternsOpen:V.length>0}),L++,a(()=>{let Z=document.getElementById("preserve-files-dialog");Z?.showModal&&!Z.open&&Z.showModal(),document.getElementById("preserve-files-search")?.focus()},0),P()},e.closePreserveFiles=()=>{document.getElementById("preserve-files-dialog")?.close?.(),e.preserve.open=!1},e.savePreserveFiles=()=>{e.options.preserveFiles=la([...Qe(),...Q()]),e.closePreserveFiles()},e.preserveRows=()=>{if(!te)return[];let S=ue(),V=(be,Ze,kn)=>{let wt=!!e.preserve.picked[be],qn=S(be);return{kind:"file",path:be,label:kn,depth:Ze,state:qn?"all":"none",locked:!wt&&qn,note:!wt&&qn?"matched by a pattern":wt&&!qn?"excluded by a pattern":""}},z=String(e.preserve.search||"").trim().toLowerCase();if(z)return te.files.filter(be=>be.toLowerCase().includes(z)).slice(0,300).map(be=>V(be,0,be));let Z=[],fe=(be,Ze)=>{let kn=te.dirs.get(be);for(let wt of kn.dirs){let qn=!!e.preserve.expanded[wt],ga=Q().includes(U(wt));Z.push({kind:"dir",path:wt,label:wt.slice(be?be.length+1:0)+"/",depth:Ze,open:qn,state:R(wt),locked:!1,note:ga?"whole folder":""}),qn&&fe(wt,Ze+1)}for(let wt of kn.files)Z.push(V(wt,Ze,wt.slice(be?be.length+1:0)))};return fe("",0),Z},e.preserveCount=()=>te?te.files.filter(C).length:0,e.togglePreserveDir=S=>{e.preserve.expanded[S]=!e.preserve.expanded[S]},e.togglePreserveFile=S=>{e.preserve.picked[S]=!e.preserve.picked[S],L++},e.togglePreserveFolder=S=>{let V=U(S),z=se(S),Z=Q().filter(be=>be!==V&&be!==z),fe=R(S)==="all"||Q().includes(V);for(let be of Pe(S))e.preserve.picked[be]=!1;L++,e.preserve.patterns=(fe?Z:[...Z,V]).join(`
`),fe&&Pe(S).some(C)&&(e.preserve.patterns=[...Z,z].join(`
`))};let G=[".github","","docs"].flatMap(S=>["README.md","README","README.markdown","README.rst","README.txt","readme.md"].map(V=>(S?S+"/":"")+V));e.readmePreserved=()=>{if(e.detectedType!=="repo")return!1;let S=(e.options?.preserveFiles||[]).map(V=>String(V).trim()).filter(Boolean);if(!S.length)return!1;try{let V=da(S);return G.some(z=>V(z))}catch{return!1}};function F(){let S={terms:e.terms&&!e.readmePreserved()?e.terms.split(`
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(S.branchName=e.source.branch);try{let V=parseGithubUrl(e.sourceUrl);S.repoName=`${V.owner}/${V.repo}`}catch{}return S}function Y(S,V,z){let Z=null,fe=0,be=()=>{if(e.detectedType!==S)return;Z&&a.cancel(Z);let Ze=++fe;e.previewStatus="loading",Z=a(()=>{Z=null;let kn=V();if(!kn){e.previewStatus="idle";return}t.post("/api/anonymize-preview",kn).then(wt=>{Ze===fe&&(z(wt.data,kn),e.previewStatus="ready")}).catch(()=>{Ze===fe&&(e.previewStatus="error")})},200)};return be.cancel=()=>{fe++,Z&&a.cancel(Z),Z=null},e.on("dispose",be.cancel),be}e.retryPreview=()=>{e.detectedType==="repo"&&M(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()};let M=Y("repo",()=>e.readme?{content:e.readme,options:F()}:null,S=>{if(typeof S?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=S.content;let V="";try{let Z=parseGithubUrl(e.sourceUrl),fe=e.source.branch||e.details&&e.details.defaultBranch||"main";V=`https://github.com/${Z.owner}/${Z.repo}/raw/${fe}/`}catch{}let z=renderMD(e.anonymize_readme,V);e.html_readme=z,a(Prism.highlightAll,150)});function H(){!e.anonymize||!e.anonymize.terms||M()}async function ee(){let S=parseGithubUrl(e.sourceUrl),V=h();try{Ye();let z=await t.get(`/api/pr/${S.owner}/${S.repo}/${S.pullRequestId}`,{params:{connection:e.githubConnection}});if(!v(V))return;e.details=z.data,e.pullRequestId||(e.pullRequestId=S.repo+"-PR"+S.pullRequestId+"-"+generateRandomId(4))}catch(z){if(!v(V))return;throw z.data&&(r("ERRORS."+z.data.error).then(Z=>{e.addToast({title:"Error",date:new Date,body:Z}),e.error=Z},console.error),lt(z.data.error)),re("sourceUrl","missing",!1),z}finally{k(V)}}let K=et(new Map);function he(){let S=new Set,V=e.details&&e.details.pullRequest;if(!V)return S;typeof V.title=="string"&&S.add(V.title),typeof V.body=="string"&&S.add(V.body),typeof V.diff=="string"&&S.add(V.diff);let z=V.comments||[];for(let Z of z)typeof Z.author=="string"&&S.add(Z.author),typeof Z.body=="string"&&S.add(Z.body);return S}let ne=Y("pr",()=>{let S=he(),V=Array.from(S);return V.length===0?null:{contents:V,options:F()}},(S,V)=>{if(!Array.isArray(S?.contents)||S.contents.length!==V.contents.length||S.contents.some(fe=>typeof fe!="string"))throw Error("Invalid preview response");let z=V.contents,Z=new Map;for(let fe=0;fe<z.length&&fe<S.contents.length;fe++)Z.set(z[fe],S.contents[fe]);K.clear(),Z.forEach((fe,be)=>K.set(be,fe))});e.anonymizePrContent=function(S){return S&&(K.has(S)?K.get(S):"")};async function pe(){let S=parseGithubUrl(e.sourceUrl),V=h();try{Ye(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let z=await t.get(`/api/gist/source/${S.gistId}`);if(!v(V))return;e.details=z.data,e.gistId||(e.gistId="gist-"+S.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(z){if(!v(V))return;if(z.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,re("sourceUrl","missing",!1);return}if(z.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,re("sourceUrl","missing",!1);return}throw z.data&&(r("ERRORS."+z.data.error).then(Z=>{e.addToast({title:"Error",date:new Date,body:Z}),e.error=Z},console.error),lt(z.data.error)),re("sourceUrl","missing",!1),z}finally{k(V)}}let ye=et(new Map);function Te(){let S=new Set,V=e.details&&e.details.gist;if(!V)return S;typeof V.description=="string"&&S.add(V.description),typeof V.ownerLogin=="string"&&S.add(V.ownerLogin);let z=V.files||[];for(let fe of z)typeof fe.filename=="string"&&S.add(fe.filename),typeof fe.content=="string"&&S.add(fe.content);let Z=V.comments||[];for(let fe of Z)typeof fe.author=="string"&&S.add(fe.author),typeof fe.body=="string"&&S.add(fe.body);return S}let Le=Y("gist",()=>{let S=Te(),V=Array.from(S);return V.length===0?null:{contents:V,options:F()}},(S,V)=>{if(!Array.isArray(S?.contents)||S.contents.length!==V.contents.length||S.contents.some(fe=>typeof fe!="string"))throw Error("Invalid preview response");let z=V.contents,Z=new Map;for(let fe=0;fe<z.length&&fe<S.contents.length;fe++)Z.set(z[fe],S.contents[fe]);ye.clear(),Z.forEach((fe,be)=>ye.set(be,fe)),Be()});e.anonymizeGistContent=function(S){return S&&(ye.has(S)?ye.get(S):"")},e.previewGistFiles=[];function Be(){let S=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=S.map(V=>({filename:e.anonymizeGistContent(V.filename),content:e.anonymizeGistContent(V.content),language:V.language}))}e.watch("details.gist.files",Be,!0),e.watch("terms",Be);function st(){let S=e.conference;if(e.conference_data=null,!S)return;let V=S===E||e.isUpdate&&S===e._originalConference;S!==E&&(E=null),t.get("/api/conferences/"+S).then(z=>{e.conference===S&&(e.conference_data=z.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!V&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.confere Line truncated
`).filter(be=>be),connection:e.githubConnection,fullName:`${z.owner}/${z.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(Z.options.pageSource=e.details.pageSource),Ye();let fe=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(fe,Z,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/status/"+e.repoId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Ze=>{e.error=Ze},console.error),lt(be.data.error),["quota_exceeded","subscription_required_for_never_expiration"].includes(be.data.error)&&(be.data.error==="subscription_required_for_never_expiration"&&(e._existingNever=!1),e.hostingQuotaBlocked=!0,e.hostingReview=!0,it(()=>document.getElementById("repository-hosting")?.scrollIntoView({block:"center"}))))}).finally(()=>{V.disabled=!1})},e.anonymizeGist=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let V=S.currentTarget||S.target.closest?.("button")||S.target;V.disabled=!0;let z=parseGithubUrl(e.sourceUrl),Z={gistId:e.gistId,terms:e.terms.trim().split(`
`).filter(be=>be),source:{gistId:z.gistId},options:e.options,conference:e.conference};Ye();let fe=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(fe,Z,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/gist/"+e.gistId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Ze=>{e.error=Ze},console.error),lt(be.data.error))}).finally(()=>{V.disabled=!1})},e.anonymizePullRequest=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let V=S.currentTarget||S.target.closest?.("button")||S.target;V.disabled=!0;let z=parseGithubUrl(e.sourceUrl),Z={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
`).filter(be=>be),source:{repositoryFullName:`${z.owner}/${z.repo}`,pullRequestId:z.pullRequestId},options:e.options,conference:e.conference};Ye();let fe=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(fe,Z,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/pr/"+e.pullRequestId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Ze=>{e.error=Ze},console.error),lt(be.data.error))}).finally(()=>{V.disabled=!1})},e.watch("conference",()=>{st()}),e.termHints=[],e.matchTermAsWritten=S=>{e.terms=Vu(e.terms,S)},e.watch("options.preserveFiles",()=>{e.detectedType==="repo"&&M()}),e.watch("terms",()=>{e.termHints=Mu(e.terms),e.detectedType==="repo"&&H(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.image",()=>{e.detectedType==="repo"&&H(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.link",()=>{e.detectedType==="repo"&&H(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("details",()=>{e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()},!0)},pa=function(e,t,s,i,o,r){let a=At();Lu();let u=vn();e.on("dark-mode",(U,se)=>{e.aceOption&&(se?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let c=0,m=null,g=null;e.fileLoadingSlow=!1,e.fileReceivedBytes=0;function v(){m&&(m.resolve(),m=null),g&&(a.timeout.cancel(g),g=null)}let h=!1;e.on("dispose",()=>{h=!0,c++,v(),k&&k.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"click",U=>{e.symbolsOpen&&!U.target.closest?.(".symbols")&&(e.symbolsOpen=!1),e.moreOpen&&!U.target.closest?.(".tb-more")&&(e.moreOpen=!1),e.tocOpen&&!U.target.closest?.(".tb-toc-m")&&(e.tocOpen=!1),e.gotoOpen&&U.target.isConnected&&!U.target.closest?.(".goto-pop, .btn-goto, .tb-more")&&(e.gotoOpen=!1),e.accountOpen&&!U.target.closest?.(".acct")&&(e.accountOpen=!1),e.helpOpen&&!U.target.closest?.(".vb-help")&&(e.helpOpen=!1)}),u(document,"keydown",U=>{U.key==="Escape"&&(e.symbolsOpen=!1,e.moreOpen=!1,e.tocOpen=!1,e.gotoOpen=!1,e.accountOpen=!1,e.helpOpen=!1),(U.ctrlKey||U.metaKey)&&!U.altKey&&U.key&&U.key.toLowerCase()==="f"&&e.type==="html"&&window.document.querySelector(".reading-layout")&&(U.preventDefault(),e.toggleMdFind(!0))}),u(document,"keydown",function(U){if((U.metaKey||U.ctrlKey)&&U.key==="k"){U.preventDefault();var se=document.querySelector(".tree-search-input");se&&(se.focus(),se.select())}});var k=null;e.onFileSearchChange=function(){k&&(k.resolve(),k=null);let U=e.fileSearchQuery;if(!U||U.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let se=r.defer();k=se,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(U)}`,{timeout:se.promise}).then(function(Q){if(!(h||k!==se)){k=null,e.fileSearchLoading=!1;var te={};e.files.forEach(function(H){te[(H.path||"")+"/"+H.name]=!0});for(var L=[],x={},B=0;B<Q.data.length;B++){var ue=Q.data[B];if(ue.path)for(var we=ue.path.split("/"),Pe="",C=0;C<we.length;C++){var R=Pe;Pe=Pe?Pe+"/"+we[C]:we[C];var P=R+"/"+we[C];!te[P]&&!x[P]&&(x[P]=!0,L.push({name:we[C],path:R}))}}L.sort(function(H,ee){return(H.path||"").split("/").length-(ee.path||"").split("/").length}),L.length>0&&e.files.push.apply(e.files,L);for(var G=[],F=0;F<Q.data.length;F++){var Y=Q.data[F],M=(Y.path||"")+"/"+Y.name;!te[M]&&Y.size!=null&&(G.push(Y),te[M]=!0)}G.length>0&&e.files.push.apply(e.files,G),e.fileSearchResults=Q.data}},function(){!h&&k===se&&(k=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let A={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},w=["license","txt"],b=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],E=["wav","mp3","ogg","wma","flac","aac","m4a"],N=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(U,se){if(e.repoId!=i.repoId)return Qe();if((i.path||"")!=e.filePath){window.matchMedia?.("(max-width: 767px)").matches&&(e.sidebarCollapsed=!0),e.filePath=i.path||"",e.paths=e.filePath.split("/").filter(Q=>Q&&Q.trim().length>0),rt();for(let Q=0;Q<e.paths.length-1;Q++){let te=Q>0?e.paths.slice(0,Q).join("/"):"";e.files.some(x=>x.path===te)||e.getFiles(te)}}});function O(){if(e.paths[0]!="")return;let U=["readme.md","readme.txt","readme.org","readme.1st","readme"],se={};for(let te of e.files)te.name.toLowerCase().indexOf("readme")>-1&&(se[te.name.toLowerCase()]=te.name);let Q=null;for(let te of U)if(se[te]){Q=te;break}if(!Q&&Object.keys(se).length>0&&(Q=Object.keys(se)[0]),Q){let te=s.url();te[te.length-1]!="/"&&(te+="/"),s.url(te+encodePathForUrl(se[Q]))}}e.fileCounts=null;let q=0,I=new Set,j=!1;e.getFiles=function(U,se=!1){let Q=e.repoId,te=q;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(U)}&v=${e.options.lastUpdateDate}`).then(function(L){if(h||Q!==e.repoId||te!==q)return[];let x=U||"";return e.files=e.file Line truncated
`).forEach((se,Q)=>{for(let[te,L]of me){let x=te.exec(se);if(x&&!/^(if|for|while|switch|return|catch)$/.test(x[1])){U.push({name:x[1],kind:L,line:Q+1});break}}}),ke={content:e.content,list:U.slice(0,300)},ke.list},e.goToSymbol=function(U){e.symbolsOpen=!1;let se=e.codeEditor;se&&(se.scrollToLine(U-1,!0,!0,()=>{}),se.gotoLine(U,0,!0),window.history.replaceState(null,"",window.location.pathname+window.location.search+"#L"+U),window.dispatchEvent(new HashChangeEvent("hashchange")))},e.gotoLine=function(){e.codeEditor&&(e.moreOpen=!1,e.gotoError="",e.gotoValue=String(e.codeEditor.getCursorPosition().row+1),e.gotoOpen=!0,setTimeout(()=>{let U=window.document.getElementById("goto-input");U&&(U.focus(),U.select())},0))},e.closeGoto=function(){e.gotoOpen=!1,e.gotoError="",e.codeEditor?.focus()},e.submitGoto=function(){let U=e.codeEditor;if(!U)return;let se=/^\s*:?\s*(\d+)\s*(?:[:,]\s*(\d+))?\s*$/.exec(e.gotoValue||""),Q=U.session.getLength();if(!se||+se[1]<1||+se[1]>Q){e.gotoError="Enter a line from 1 to "+Q;return}U.gotoLine(+se[1],se[2]?Math.max(0,+se[2]-1):0,!0),e.gotoOpen=!1,e.gotoError="",U.focus()},e.rootEntries=function(){return[...e.files||[]].sort((U,se)=>{let Q=U.size==null,te=se.size==null;return Q!==te?Q?-1:1:U.name.localeCompare(se.name)})},e.formatSize=function(U){if(U==null)return"";if(U<1024)return U+" B";let se=U/1024;return se<1024?se.toFixed(1)+" KB":(se/1024).toFixed(1)+" MB"},e.fileSize=function(U){if(!U)return null;let se=U.replace(/^\/+/,""),Q=(e.files||[]).find(te=>(te.path?te.path+"/":"")+te.name===se);return Q&&Q.size!=null?Q.size:null},e.revealFolder=function(U){e.sidebarHidden=!1,e.sidebarCollapsed=!1,it(()=>{let se=window.document.querySelector('.files a[data-path="/'+(window.CSS?.escape?window.CSS.escape(U):U)+'"]');se&&(se.closest("li")?.classList.contains("open")||se.click(),se.scrollIntoView({block:"nearest"}),se.focus())})},e.redactionCount=function(){return typeof e.content!="string"?0:((e.type==="html"?e.content.replace(/<[^>]*>/g," "):e.content).match(Po)||[]).length},e.renderRedacted=$u;let _e=U=>U.toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").slice(0,60)||"section";e.renderReadme=function(U){let se=e.renderRedacted(U);if(!se||typeof window.DOMParser!="function")return se;let Q=new window.DOMParser().parseFromString("<div>"+se+"</div>","text/html"),te={};return Q.querySelectorAll("h1, h2, h3").forEach(L=>{let x=L.id||_e(L.textContent);te[x]?x+="-"+ ++te[x]:te[x]=1,L.id=x}),Q.body.firstChild.innerHTML};let Ne={content:null,list:[]};e.readmeToc=function(){if(e.type!=="html"||typeof e.content!="string")return[];if(Ne.content===e.content)return Ne.list;let se=[...new window.DOMParser().parseFromString("<div>"+e.renderReadme(e.sanitize?e.sanitize(e.content):e.content)+"</div>","text/html").querySelectorAll("h2, h3")].map(Q=>({id:Q.id,text:Q.textContent.trim(),level:Q.tagName==="H3"?3:2})).slice(0,40);return Ne={content:e.content,list:se},se},e.activeHeading=null,u(window,"scroll",()=>{let U=e.readmeToc?e.readmeToc():[];if(!U.length)return;let se=150,Q="",te="";for(let L of U){let x=window.document.getElementById(L.id);if(!x)continue;let B=x.getBoundingClientRect().top;B<se?Q=L.id:!te&&B<window.innerHeight-80&&(te=L.id)}te&&(Q=te),e.activeHeading!==Q&&(e.activeHeading=Q)},{capture:!0,passive:!0}),e.scrollToHeading=function(U){if(!U){(window.document.querySelector(".explorer-content, .file-card-body, .main-pane")||window).scrollTo({top:0,behavior:"smooth"}),e.activeHeading="";return}let se=window.document.getElementById(U);se&&se.scrollIntoView({behavior:"smooth",block:"start"}),e.activeHeading=U},e.redactParts=function(U){let se=[],Q=0,te,L=/\bXXXX-(\d+)\b/g;for(U=String(U||"");te=L.exec(U);)te.index>Q&&se.push({t:U.slice(Q,te.index)}),se.push({n:te[1]}),Q=te.index+te[0].length;return Q<U.length&&se.push({t:U.slice(Q)}),se},e.pathCopied=!1,e.copyPath=async function(){window.navigator.clipboard?.writeText&&(await window.navigator.clipboard.writeText(e.filePath||""),e.pathCopied=!0,setTimeout(()=>{e.pathCopied=!1},1600))},e.contentCopied=!1,e.copyContent=async function(){if(typeof e.content!="string"||!window.navigator.clipboard?.writeText)return;let U=e.content;if(e.type==="html"&&e.url)try{U=await(await window.fetch(e.url,{credentials:"same-origin"})).text()}catch{return}await window.navigator.clipboard.writeText(U),e.contentCopied=!0,setTimeout(()=>{e.contentCopied=!1},1600)};function Re(U){return A[U]?A[U]:U}function Je(U){return U=="pdf"?"pdf":U=="html"||U=="htm"?"html-doc":U=="md"?"md":U=="org"?"org":U=="ipynb"?"IPython":w.indexOf(U)>-1?"text":b.indexOf(U)>-1?"image":N.indexOf(U)>-1?"media":E.indexOf(U)>-1?"audio":"code"}function Ke(U,se){let Q=c;if(!U){e.type="error",e.content="no_file_selected";return}let te=e.type;(te==="code"||te==="text")&&Mo().catch(()=>{}),e.type="loading",e.content="loading",e.fileLoadingSlow=!1,e.fileReceivedBytes=0;let L=r.defer();m=L,g=a.timeout(()=>{!h&&Q===c&&e.type==="loading"&&(e.fileLoadingSlow=!0)},1500);let x=se&&se.sha|| Line truncated
Contact: jane.smith@mit.edu`,e.demoTerms="Jane Smith, Smith, MIT CSAIL, jane.smith@mit.edu",e.demoParts=function(){let u=(e.demoTerms||"").split(/[,\n]/).map(m=>m.trim()).filter(Boolean).sort((m,k)=>k.length-m.length),p=m=>m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!u.length)return[{text:e.demoText}];let v=new RegExp("("+u.map(p).join("|")+")","gi"),h={},g=0;return(e.demoText||"").split(v).filter(m=>m!=="").map(m=>{let k=u.find(T=>T.toLowerCase()===m.toLowerCase());return k?(h[k.toLowerCase()]||(h[k.toLowerCase()]=++g),{text:String(h[k.toLowerCase()]),redacted:!0,original:m}):{text:m}})},e.demoCount=()=>e.demoParts().filter(u=>u.redacted).length,e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function r(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests}).catch(()=>{})}r();function a(u){let p={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return p;let v=new Array(u.length-1);for(let w=1;w<u.length;w++)v[w-1]=u[w]-u[w-1];let h=v.length,g=Math.max.apply(null,v),m=Math.min.apply(null,v),k=Math.min(0,m),T=g-k||1;p.viewW=h*2,p.bars=new Array(h);for(let w=0;w<h;w++){let b=(v[w]-k)/T,E=Math.max(1.5,b*34);p.bars[w]={x:(w*2+.25).toFixed(2),y:(36-E).toFixed(2),w:"1.5",h:E.toFixed(2)}}if(p.deltaToday=v[h-1],h>=2){let w=v[h-2];w&&(p.pctChange=(p.deltaToday-w)/w*100)}return p.pctAbs=Math.round(Math.abs(p.pctChange)),p.isUp=p.pctChange>=0,p}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let p=u.data||[];e.history={repositories:a(p.map(v=>v.nbRepositories||0)),users:a(p.map(v=>v.nbUsers||0)),pageViews:a(p.map(v=>v.nbPageViews||0)),pullRequests:a(p.map(v=>v.nbPullRequests||0))}}).catch(()=>{})},Ku=function(e,t,o,i,s,r){let a=Tt();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,nt(()=>document.querySelector(".dashboard-quota > summary")?.focus())},bn()(document,"click",D=>{e.quotaExpanded&&!D.target.closest?.(".dashboard-quota")&&(e.quotaExpanded=!1)}),e.filtersExpanded=!1,e.projectView="all",e.selectedProject=null,e.projectName="",e.nameSaving=!1,e.nameError="",e.nameMessage="",e.needsAttention=D=>D.status==="error"||D._broken||p(D.status)==="progress"&&D._stale,e.attentionCount=()=>e.dashboardTotals?.attention??e.items.filter(e.needsAttention).length,e.hasProjects=()=>(e.dashboardTotals?.total??e.items.length)>0,e.setProjectView=D=>{e.projectView=D,D==="attention"&&(e.filters.status.error=!0,e.filters.status.progress=!0)},e.projectDetailsId=D=>"project-details-"+encodeURIComponent(D._type+":"+(D._id||D._source||"unknown")),e.showProjectDetails=async D=>{e.selectedProject=D,e.projectName=D.projectName||"",e.nameError="",e.nameMessage="",await nt();let X=document.getElementById(e.projectDetailsId(D));X?.focus(),X?.scrollIntoView?.({block:"nearest"})},e.closeProjectDetails=D=>{e.selectedProject=null,nt(()=>document.querySelector(`[data-project-actions="${encodeURIComponent(D._type+":"+D._id)}"]`)?.focus())},e.saveProjectName=async D=>{if(e.nameSaving||!D._id)return;let X=e.projectName.trim();if(X.length>100){e.nameError="Use 100 characters or fewer.";return}e.nameSaving=!0,e.nameError="",e.nameMessage="";try{let le=await t.post("/api/user/project-name",{type:D._type,id:D._id,name:X});D.projectName=le.data.name,D._label=D.projectName||D._fallbackLabel,e.selectedProject===D&&(e.nameMessage=X?"Project name saved.":"Default project name restored."),N||(e.dashboardCursor=null,await q(),e.selectedProject===D&&(e.selectedProject=e.items.find(Ee=>Ee._type===D._type&&Ee._id===D._id)||null))}catch{e.selectedProject===D&&(e.nameError="The project name could not be saved. Try again.")}finally{e.nameSaving=!1}},e.copyingId=null,e.copyLink=async D=>{if(!D._viewUrl||e.copyingId)return;let X=new URL(D._viewUrl,s.location.origin).href;e.copyingId=D._type+":"+D._id;try{await Bu(X),e.addToast({title:"Link copied",body:"The anonymous link is ready to share."})}catch{e.addToast({title:"Copy this anonymous link",body:X})}finally{e.copyingId=null}},e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"In progress",error:"Error",expired:"Expired",removed:"Removed"};let u=["queue","download","downloaded","preparing","anonymizing"];function p(D){return D==="ready"||D==="error"?D:D==="expired"||D==="expiring"?"expired":D==="removed"||D==="removing"?"removed":(u.indexOf(D)>-1,"progress")}let v=7200*1e3,h="dashboard.filterPrefs",g={typeFilter:"all",filters:{status:{ready:!0,progress:!0,error:!0,e Line truncated
`)),e.options=Object.assign({},e.options,F.options),e.options.expirationDate=F.options&&F.options.expirationDate?new Date(F.options.expirationDate):G(),!i.repoId&&!i.pullRequestId&&!i.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),S&&S()})}function ae(S,_,F){e.anonymize&&e.anonymize[S]&&e.anonymize[S].setValidity(_,F)}function he(S){try{let _=parseGithubUrl(S);if(_&&_.owner&&_.repo)return _.owner+"/"+_.repo}catch{}return null}e.accessRepairAllowed=!1,e.repairingGitHubAccess=!1,e.accessRepairMessage="",e.repairGitHubAccess=async()=>{if(!e.accessRepairAllowed||e.repairingGitHubAccess||!e.githubConnections?.appEnabled||!i.repoId||e.role!=="owner"||e.githubConnection!=="github-app"||he(e.sourceUrl)!==e._originalFullName)return;e.repairingGitHubAccess=!0,e.accessRepairMessage="";let S=m();try{let _=await t.get("/github/connections");if(e.githubConnections=_.data,!_.data.appEnabled)return;let F=await t.post("/github/connections/migrate",{type:"repository",id:i.repoId,connection:"github-app"},{headers:{"X-CSRF-Token":_.data.csrf}});if(!g(S))return;e.error=null,F.data.fullName&&(e._originalFullName=F.data.fullName,e.source.fullName=F.data.fullName,e.sourceUrl="https://github.com/"+F.data.fullName),e.accessRepairMessage="GitHub access repaired. Your anonymous URL and published commit are unchanged.",await e.urlSelected(!0)}catch{g(S)&&(e.accessRepairMessage="Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced.")}finally{e.repairingGitHubAccess=!1,k(S)}},e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await Re()}catch{}}};function Ce(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:he(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function Pe(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function Ne(){let S=new URLSearchParams(window.location.search).get("billingDraft");if(S){b.restore();try{let F=(await t.get("/api/billing/draft/"+encodeURIComponent(S))).data;if(F.path!==s.path())throw new Error("Wrong task");E=F.values.conference||null;for(let Q of["sourceUrl","terms","repoId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(F.values,Q)&&(e[Q]=F.values[Q]);return e.options?.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),N=F.id,O=JSON.stringify(b.checkpoint()),e.draftRestored=!0,e.hostingReview=!0,e.sourceUrl&&await Pe(),s.replace?.(s.path()),!0}catch{e.billingError="The checkout draft could not be restored. Retry by reloading this page; any local draft is still available."}}let _;try{_=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!_||_.path!==s.path()||Date.now()-_.savedAt>=30*6e4)return b.restore()?(e.sourceUrl&&await Pe(),!0):!1;b.restore();for(let F of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(_.draft,F)&&(e[F]=_.draft[F]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await Pe(),sessionStorage.removeItem("github-access-draft"),!0}Ee(()=>{!i.repoId&&!i.pullRequestId&&!i.gistId&&a(Ne,0),i.repoId&&i.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=i.repoId,t.get("/api/repo/"+e.repoId).then(async S=>{e.accessRepairAllowed=!["removed","archived","preparing","queue","download","removing","expiring"].includes(S.data.status),e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.fullName,e._originalFullName=S.data.source.fullName,e.terms=S.data.options.terms.filter(_=>_).join(`
`),e.source=S.data.source,e.role=S.data.role||"owner",e._existingNever=S.data.options.expirationMode==="never",e._savedExpirationDate=S.data.options.expirationDate?new Date(S.data.options.expirationDate):null,e.coauthors=S.data.coauthors||[],e._originalBranch=S.data.source.branch,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,e.repositoryID=S.data.source.repositoryID,e._originalRepositoryID=S.data.source.repositoryID,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&(await Promise.all([Re(),Qe()]),j())},()=>{s.url("/404")})),i.pullRequestId&&i.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=i.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async S=>{if(e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.repositoryFullName+"/pull/"+S.data.source.pullRequestId,e.terms=S.data.options.terms.filter(_=>_).join(`
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne())try{await ee()}catch{}},()=>{s.url("/404")})),i.gistId&&i.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=i.gistId,t.get("/api/gist/"+e.gistId).then(async S=>{e.sourceUrl="https://gist.github.com/"+S.data.source.gistId,e.terms=S.data.options.terms.filter(_=>_).join(`
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&await fe()},()=>{s.url("/404")}))}),t.get("/github/connections").then(S=>{e.githubConnections=S.data,!i.repoId&&!i.pullRequestId&&!i.gistId&&e.githubConnection===void 0&&S.data.appEnabled&&S.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&Pe())}).catch(()=>{}),e.urlSelected=async(S=!1)=>{let _=++p;e.previousConfigurations=[],e.previousConfigurationId="";let F;try{F=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,V.cancel(),ne.cancel(),Le.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",ae("sourceUrl","github",!1);return}let Q=w(F),pe=S||T===Q;T=Q,e.sourceUrlError="",ae("sourceUrl","github",!0);let be=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,setTimeout(()=>{let Xe=window.document.getElementById("sourceUrl");if(Xe){try{Xe.setSelectionRange(0,0)}catch{}Xe.scrollLeft=0}},50),be&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!pe&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],pe||(e.source={type:"GitHubStream",branch:"",commit:""}),V.cancel(),ne.cancel(),Le.cancel(),K.clear(),ye.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=pe;try{F.gistId&&!F.repo?(e.detectedType="gist",e.source={gistId:F.gistId},await fe()):F.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:F.owner+"/"+F.repo,pullRequestId:F.pullRequestId},await ee()):(e.detectedType="repo",le(),await Promise.all([Re(),Qe()]),j())}catch{}finally{_===p&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let S=e.branches.filter(F=>F.name==e.source.branch)[0];if(!S)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=S.commit),e.readme=S.readme,await Qe(),j()}),e.getBranches=async S=>{let _=parseGithubUrl(e.sourceUrl),F=m();try{let Q=await t.get(`/api/repo/${_.owner}/${_.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0||e.reconnectingSource?"1":"0",repositoryID:Ce()}});if(!g(F))return;e.branches=Q.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let pe=e.branches.filter(be=>be.name==e.source.branch);pe.length>0&&(!(!e.reconnectingSource&&!S&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=pe[0].commit),e.readme=pe[0].readme,await Qe(S))}catch(Q){if(!g(F))return;e.branches=[],e.sourceUnreachable=Q&&(Q.status===404||Q.data&&Q.data.error==="repo_not_found");let pe=Q&&Q.data&&Q.data.error||(Q&&Q.status===404?"repo_not_found":"unknown_error");r("ERRORS."+pe).then(be=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:be}),e.error=be},console.error),typeof ae=="function"&&ae("sourceUrl","missing",!1)}finally{k(F)}};async function Re(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye();let F=await t.get(`/api/repo/${S.owner}/${S.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,repositoryID:Ce(),force:"1"}});if(!g(_))return;e.details=F.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(F){if(!g(_))return;throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}async function Qe(S){if(e.readme&&!S)return e.readme;let _=parseGithubUrl(e.sourceUrl),F=m();try{let Q=await t.get(`/api/repo/${_.owner}/${_.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0?"1":"0",branch:e.source.branch,repositoryID:Ce()}});g(F,!0)&&(e.readme=Q.data)}catch{g(F,!0)&&(e.readme="")}finally{k(F)}}let Ke={magicalBraces:!0},rt=S=>S.startsWith("/")&&!S.endsWith("/")&&On(so(S.slice(1),Ke),Ke)===S.slice(1),Ze=()=>Object.keys(e.preserve.picked).filter(S=>e. Line truncated
`).map(S=>S.trim()).filter(Boolean),te=null,L=0,x=null,H=()=>!1;function ue(){let S=L+"\0"+e.preserve.patterns;if(S!==x){x=S;try{H=ua(da([...Ze(),...Z()]))}catch{H=()=>!1}}return H}function we(S){let _=new Map([["",{dirs:[],files:[]}]]);for(let F of S){let Q=F.split("/"),pe="";for(let be=0;be<Q.length-1;be++){let Xe=Q.slice(0,be+1).join("/");_.has(Xe)||(_.set(Xe,{dirs:[],files:[]}),_.get(pe).dirs.push(Xe)),pe=Xe}_.get(pe).files.push(F)}for(let F of _.values())F.dirs.sort();return{files:S,dirs:_,descendants:new Map}}function Oe(S){return te.descendants.has(S)||te.descendants.set(S,te.files.filter(_=>_.startsWith(S+"/"))),te.descendants.get(S)}let C=S=>ue()(S);function R(S){let _=Oe(S),F=0;for(let Q of _)C(Q)&&F++;return F===0?"none":F===_.length?"all":"some"}e.preserve={open:!1,loading:!1,error:"",truncated:!1,commit:"",search:"",expanded:{},picked:{},patterns:"",patternsOpen:!1},e.preserveSummary=()=>{let S=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean).map(_=>rt(_)?so(_.slice(1),Ke):_);return S.length?S.slice(0,2).join(", ")+(S.length>2?` and ${S.length-2} more`:""):"None. Every file is redacted."};async function I(){let S=e.source?.commit;if(!(te&&e.preserve.commit===S)){e.preserve.loading=!0,e.preserve.error="",te=null;try{let _=parseGithubUrl(e.sourceUrl),F=await t.get(`/api/repo/${_.owner}/${_.repo}/tree`,{params:{commit:S,connection:e.githubConnection,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,repositoryID:Ce()}});if(e.source?.commit!==S)return;te=go(we(F.data.files||[])),e.preserve.commit=S,e.preserve.truncated=!!F.data.truncated}catch{e.preserve.error="The file list could not be loaded from GitHub. You can still use patterns.",e.preserve.patternsOpen=!0}finally{e.preserve.loading=!1}}}e.openPreserveFiles=()=>{let S={},_=[];for(let Q of e.options?.preserveFiles||[]){let pe=String(Q).trim();pe&&(!_.length&&rt(pe)?S[so(pe.slice(1),Ke)]=!0:_.push(pe))}let F={};for(let Q of Object.keys(S)){let pe=Q.split("/");for(let be=1;be<pe.length;be++)F[pe.slice(0,be).join("/")]=!0}Object.assign(e.preserve,{open:!0,search:"",picked:S,expanded:F,patterns:_.join(`
`),patternsOpen:_.length>0}),L++,a(()=>{let Q=document.getElementById("preserve-files-dialog");Q?.showModal&&!Q.open&&Q.showModal(),document.getElementById("preserve-files-search")?.focus()},0),I()},e.closePreserveFiles=()=>{document.getElementById("preserve-files-dialog")?.close?.(),e.preserve.open=!1},e.savePreserveFiles=()=>{e.options.preserveFiles=da([...Ze(),...Z()]),e.closePreserveFiles()},e.preserveRows=()=>{if(!te)return[];let S=ue(),_=(be,Xe,Yt)=>{let wt=!!e.preserve.picked[be],$n=S(be);return{kind:"file",path:be,label:Yt,depth:Xe,state:$n?"all":"none",locked:!wt&&$n,note:!wt&&$n?"matched by a pattern":wt&&!$n?"excluded by a pattern":""}},F=String(e.preserve.search||"").trim().toLowerCase();if(F)return te.files.filter(be=>be.toLowerCase().includes(F)).slice(0,300).map(be=>_(be,0,be));let Q=[],pe=(be,Xe)=>{let Yt=te.dirs.get(be);for(let wt of Yt.dirs){let $n=!!e.preserve.expanded[wt],va=Z().includes(U(wt));Q.push({kind:"dir",path:wt,label:wt.slice(be?be.length+1:0)+"/",depth:Xe,open:$n,state:R(wt),locked:!1,note:va?"whole folder":""}),$n&&pe(wt,Xe+1)}for(let wt of Yt.files)Q.push(_(wt,Xe,wt.slice(be?be.length+1:0)))};return pe("",0),Q},e.preserveCount=()=>te?te.files.filter(C).length:0,e.togglePreserveDir=S=>{e.preserve.expanded[S]=!e.preserve.expanded[S]},e.togglePreserveFile=S=>{e.preserve.picked[S]=!e.preserve.picked[S],L++},e.togglePreserveFolder=S=>{let _=U(S),F=oe(S),Q=Z().filter(be=>be!==_&&be!==F),pe=R(S)==="all"||Z().includes(_);for(let be of Oe(S))e.preserve.picked[be]=!1;L++,e.preserve.patterns=(pe?Q:[...Q,_]).join(`
`),pe&&Oe(S).some(C)&&(e.preserve.patterns=[...Q,F].join(`
`))};let W=[".github","","docs"].flatMap(S=>["README.md","README","README.markdown","README.rst","README.txt","readme.md"].map(_=>(S?S+"/":"")+_));e.readmePreserved=()=>{if(e.detectedType!=="repo")return!1;let S=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean);if(!S.length)return!1;try{let _=ua(S);return W.some(F=>_(F))}catch{return!1}};function z(){let S={terms:e.terms&&!e.readmePreserved()?e.terms.split(`
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(S.branchName=e.source.branch);try{let _=parseGithubUrl(e.sourceUrl);S.repoName=`${_.owner}/${_.repo}`}catch{}return S}function Y(S,_,F){let Q=null,pe=0,be=()=>{if(e.detectedType!==S)return;Q&&a.cancel(Q);let Xe=++pe;e.previewStatus="loading",Q=a(()=>{Q=null;let Yt=_();if(!Yt){e.previewStatus="idle";return}t.post("/api/anonymize-preview",Yt).then(wt=>{Xe===pe&&(F(wt.data,Yt),e.previewStatus="ready")}).catch(()=>{Xe===pe&&(e.previewStatus="error")})},200)};return be.cancel=()=>{pe++,Q&&a.cancel(Q),Q=null},e.on("dispose",be.cancel),be}e.retryPreview=()=>{e.detectedType==="repo"&&V(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()};let V=Y("repo",()=>e.readme?{content:e.readme,options:z()}:null,S=>{if(typeof S?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=S.content;let _="";try{let Q=parseGithubUrl(e.sourceUrl),pe=e.source.branch||e.details&&e.details.defaultBranch||"main";_=`https://github.com/${Q.owner}/${Q.repo}/raw/${pe}/`}catch{}let F=renderMD(e.anonymize_readme,_);e.html_readme=F,a(Prism.highlightAll,150)});function j(){!e.anonymize||!e.anonymize.terms||V()}async function ee(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye();let F=await t.get(`/api/pr/${S.owner}/${S.repo}/${S.pullRequestId}`,{params:{connection:e.githubConnection}});if(!g(_))return;e.details=F.data,e.pullRequestId||(e.pullRequestId=S.repo+"-PR"+S.pullRequestId+"-"+generateRandomId(4))}catch(F){if(!g(_))return;throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}let K=et(new Map);function me(){let S=new Set,_=e.details&&e.details.pullRequest;if(!_)return S;typeof _.title=="string"&&S.add(_.title),typeof _.body=="string"&&S.add(_.body),typeof _.diff=="string"&&S.add(_.diff);let F=_.comments||[];for(let Q of F)typeof Q.author=="string"&&S.add(Q.author),typeof Q.body=="string"&&S.add(Q.body);return S}let ne=Y("pr",()=>{let S=me(),_=Array.from(S);return _.length===0?null:{contents:_,options:z()}},(S,_)=>{if(!Array.isArray(S?.contents)||S.contents.length!==_.contents.length||S.contents.some(pe=>typeof pe!="string"))throw Error("Invalid preview response");let F=_.contents,Q=new Map;for(let pe=0;pe<F.length&&pe<S.contents.length;pe++)Q.set(F[pe],S.contents[pe]);K.clear(),Q.forEach((pe,be)=>K.set(be,pe))});e.anonymizePrContent=function(S){return S&&(K.has(S)?K.get(S):"")};async function fe(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let F=await t.get(`/api/gist/source/${S.gistId}`);if(!g(_))return;e.details=F.data,e.gistId||(e.gistId="gist-"+S.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(F){if(!g(_))return;if(F.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,ae("sourceUrl","missing",!1);return}if(F.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,ae("sourceUrl","missing",!1);return}throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}let ye=et(new Map);function Ae(){let S=new Set,_=e.details&&e.details.gist;if(!_)return S;typeof _.description=="string"&&S.add(_.description),typeof _.ownerLogin=="string"&&S.add(_.ownerLogin);let F=_.files||[];for(let pe of F)typeof pe.filename=="string"&&S.add(pe.filename),typeof pe.content=="string"&&S.add(pe.content);let Q=_.comments||[];for(let pe of Q)typeof pe.author=="string"&&S.add(pe.author),typeof pe.body=="string"&&S.add(pe.body);return S}let Le=Y("gist",()=>{let S=Ae(),_=Array.from(S);return _.length===0?null:{contents:_,options:z()}},(S,_)=>{if(!Array.isArray(S?.contents)||S.contents.length!==_.contents.length||S.contents.some(pe=>typeof pe!="string"))throw Error("Invalid preview response");let F=_.contents,Q=new Map;for(let pe=0;pe<F.length&&pe<S.contents.length;pe++)Q.set(F[pe],S.contents[pe]);ye.clear(),Q.forEach((pe,be)=>ye.set(be,pe)),Be()});e.anonymizeGistContent=function(S){return S&&(ye.has(S)?ye.get(S):"")},e.previewGistFiles=[];function Be(){let S=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=S.map(_=>({filename:e.anonymizeGistContent(_.filename),content:e.anonymizeGistContent(_.content),language:_.language}))}e.watch("details.gist.files",Be,!0),e.watch("terms",Be);function st(){let S=e.conference;if(e.conference_data=null,!S)return;let _=S===E||e.isUpdate&&S===e._originalConference;S!==E&&(E=null),t.get("/api/conferences/"+S).then(F=>{e.conference===S&&(e.conference_data=F.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!_&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.confere Line truncated
`).filter(be=>be),connection:e.githubConnection,fullName:`${F.owner}/${F.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(Q.options.pageSource=e.details.pageSource),Ye();let pe=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/status/"+e.repoId},be=>{if(be.data){let Xe=["quota_exceeded","subscription_required_for_never_expiration"].includes(be.data.error);Xe||r("ERRORS."+be.data.error).then(Yt=>{e.error=Yt},console.error),lt(be.data.error),Xe&&(be.data.error==="subscription_required_for_never_expiration"&&(e._existingNever=!1),e.hostingQuotaBlocked=!0,e.hostingReview=!0,nt(()=>document.getElementById("repository-hosting")?.scrollIntoView({block:"center"})))}}).finally(()=>{_.disabled=!1})},e.anonymizeGist=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=S.currentTarget||S.target.closest?.("button")||S.target;_.disabled=!0;let F=parseGithubUrl(e.sourceUrl),Q={gistId:e.gistId,terms:e.terms.trim().split(`
`).filter(be=>be),source:{gistId:F.gistId},options:e.options,conference:e.conference};Ye();let pe=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/gist/"+e.gistId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Xe=>{e.error=Xe},console.error),lt(be.data.error))}).finally(()=>{_.disabled=!1})},e.anonymizePullRequest=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=S.currentTarget||S.target.closest?.("button")||S.target;_.disabled=!0;let F=parseGithubUrl(e.sourceUrl),Q={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
`).filter(be=>be),source:{repositoryFullName:`${F.owner}/${F.repo}`,pullRequestId:F.pullRequestId},options:e.options,conference:e.conference};Ye();let pe=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/pr/"+e.pullRequestId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Xe=>{e.error=Xe},console.error),lt(be.data.error))}).finally(()=>{_.disabled=!1})},e.watch("conference",()=>{st()}),e.termHints=[],e.matchTermAsWritten=S=>{e.terms=qu(e.terms,S)},e.watch("options.preserveFiles",()=>{e.detectedType==="repo"&&V()}),e.watch("terms",()=>{e.termHints=Vu(e.terms),e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.image",()=>{e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.link",()=>{e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("details",()=>{e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()},!0)},fa=function(e,t,o,i,s,r){let a=Tt();Uu();let u=bn();e.on("dark-mode",(U,oe)=>{e.aceOption&&(oe?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let p=0,v=null,h=null;e.fileLoadingSlow=!1,e.fileReceivedBytes=0;function g(){v&&(v.resolve(),v=null),h&&(a.timeout.cancel(h),h=null)}let m=!1;e.on("dispose",()=>{m=!0,p++,g(),k&&k.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"click",U=>{e.symbolsOpen&&!U.target.closest?.(".symbols")&&(e.symbolsOpen=!1),e.moreOpen&&!U.target.closest?.(".tb-more")&&(e.moreOpen=!1),e.tocOpen&&!U.target.closest?.(".tb-toc-m")&&(e.tocOpen=!1),e.gotoOpen&&U.target.isConnected&&!U.target.closest?.(".goto-pop, .btn-goto, .tb-more")&&(e.gotoOpen=!1),e.accountOpen&&!U.target.closest?.(".acct")&&(e.accountOpen=!1),e.helpOpen&&!U.target.closest?.(".vb-help")&&(e.helpOpen=!1)}),u(document,"keydown",U=>{U.key==="Escape"&&(e.symbolsOpen=!1,e.moreOpen=!1,e.tocOpen=!1,e.gotoOpen=!1,e.accountOpen=!1,e.helpOpen=!1),(U.ctrlKey||U.metaKey)&&!U.altKey&&U.key&&U.key.toLowerCase()==="f"&&e.type==="html"&&window.document.querySelector(".reading-layout")&&(U.preventDefault(),e.toggleMdFind(!0))}),u(document,"keydown",function(U){if((U.metaKey||U.ctrlKey)&&U.key==="k"){U.preventDefault();var oe=document.querySelector(".tree-search-input");oe&&(oe.focus(),oe.select())}});var k=null;e.onFileSearchChange=function(){k&&(k.resolve(),k=null);let U=e.fileSearchQuery;if(!U||U.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let oe=r.defer();k=oe,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(U)}`,{timeout:oe.promise}).then(function(Z){if(!(m||k!==oe)){k=null,e.fileSearchLoading=!1;var te={};e.files.forEach(function(j){te[(j.path||"")+"/"+j.name]=!0});for(var L=[],x={},H=0;H<Z.data.length;H++){var ue=Z.data[H];if(ue.path)for(var we=ue.path.split("/"),Oe="",C=0;C<we.length;C++){var R=Oe;Oe=Oe?Oe+"/"+we[C]:we[C];var I=R+"/"+we[C];!te[I]&&!x[I]&&(x[I]=!0,L.push({name:we[C],path:R}))}}L.sort(function(j,ee){return(j.path||"").split("/").length-(ee.path||"").split("/").length}),L.length>0&&e.files.push.apply(e.files,L);for(var W=[],z=0;z<Z.data.length;z++){var Y=Z.data[z],V=(Y.path||"")+"/"+Y.name;!te[V]&&Y.size!=null&&(W.push(Y),te[V]=!0)}W.length>0&&e.files.push.apply(e.files,W),e.fileSearchResults=Z.data}},function(){!m&&k===oe&&(k=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let T={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},w=["license","txt"],b=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],E=["wav","mp3","ogg","wma","flac","aac","m4a"],N=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(U,oe){if(e.repoId!=i.repoId)return Ze();if((i.path||"")!=e.filePath){window.matchMedia?.("(max-width: 767px)").matches&&(e.sidebarCollapsed=!0),e.filePath=i.path||"",e.paths=e.filePath.split("/").filter(Z=>Z&&Z.trim().length>0),rt();for(let Z=0;Z<e.paths.length-1;Z++){let te=Z>0?e.paths.slice(0,Z).join("/"):"";e.files.some(x=>x.path===te)||e.getFiles(te)}}});function O(){if(e.paths[0]!="")return;let U=["readme.md","readme.txt","readme.org","readme.1st","readme"],oe={};for(let te of e.files)te.name.toLowerCase().indexOf("readme")>-1&&(oe[te.name.toLowerCase()]=te.name);let Z=null;for(let te of U)if(oe[te]){Z=te;break}if(!Z&&Object.keys(oe).length>0&&(Z=Object.keys(oe)[0]),Z){let te=o.url();te[te.length-1]!="/"&&(te+="/"),o.url(te+encodePathForUrl(oe[Z]))}}e.fileCounts=null;let q=0,M=new Set,G=!1;e.getFiles=function(U,oe=!1){let Z=e.repoId,te=q;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(U)}&v=${e.options.lastUpdateDate}`).then(function(L){if(m||Z!==e.repoId||te!==q)return[];let x=U||"";return e.files=e.file Line truncated
`).forEach((oe,Z)=>{for(let[te,L]of he){let x=te.exec(oe);if(x&&!/^(if|for|while|switch|return|catch)$/.test(x[1])){U.push({name:x[1],kind:L,line:Z+1});break}}}),Ce={content:e.content,list:U.slice(0,300)},Ce.list},e.goToSymbol=function(U){e.symbolsOpen=!1;let oe=e.codeEditor;oe&&(oe.scrollToLine(U-1,!0,!0,()=>{}),oe.gotoLine(U,0,!0),window.history.replaceState(null,"",window.location.pathname+window.location.search+"#L"+U),window.dispatchEvent(new HashChangeEvent("hashchange")))},e.gotoLine=function(){e.codeEditor&&(e.moreOpen=!1,e.gotoError="",e.gotoValue=String(e.codeEditor.getCursorPosition().row+1),e.gotoOpen=!0,setTimeout(()=>{let U=window.document.getElementById("goto-input");U&&(U.focus(),U.select())},0))},e.closeGoto=function(){e.gotoOpen=!1,e.gotoError="",e.codeEditor?.focus()},e.submitGoto=function(){let U=e.codeEditor;if(!U)return;let oe=/^\s*:?\s*(\d+)\s*(?:[:,]\s*(\d+))?\s*$/.exec(e.gotoValue||""),Z=U.session.getLength();if(!oe||+oe[1]<1||+oe[1]>Z){e.gotoError="Enter a line from 1 to "+Z;return}U.gotoLine(+oe[1],oe[2]?Math.max(0,+oe[2]-1):0,!0),e.gotoOpen=!1,e.gotoError="",U.focus()},e.rootEntries=function(){return[...e.files||[]].sort((U,oe)=>{let Z=U.size==null,te=oe.size==null;return Z!==te?Z?-1:1:U.name.localeCompare(oe.name)})},e.formatSize=function(U){if(U==null)return"";if(U<1024)return U+" B";let oe=U/1024;return oe<1024?oe.toFixed(1)+" KB":(oe/1024).toFixed(1)+" MB"},e.fileSize=function(U){if(!U)return null;let oe=U.replace(/^\/+/,""),Z=(e.files||[]).find(te=>(te.path?te.path+"/":"")+te.name===oe);return Z&&Z.size!=null?Z.size:null},e.revealFolder=function(U){e.sidebarHidden=!1,e.sidebarCollapsed=!1,nt(()=>{let oe=window.document.querySelector('.files a[data-path="/'+(window.CSS?.escape?window.CSS.escape(U):U)+'"]');oe&&(oe.closest("li")?.classList.contains("open")||oe.click(),oe.scrollIntoView({block:"nearest"}),oe.focus())})},e.redactionCount=function(){return typeof e.content!="string"?0:((e.type==="html"?e.content.replace(/<[^>]*>/g," "):e.content).match(Os)||[]).length},e.renderRedacted=Lu;let Pe=U=>U.toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").slice(0,60)||"section";e.renderReadme=function(U){let oe=e.renderRedacted(U);if(!oe||typeof window.DOMParser!="function")return oe;let Z=new window.DOMParser().parseFromString("<div>"+oe+"</div>","text/html"),te={};return Z.querySelectorAll("h1, h2, h3").forEach(L=>{let x=L.id||Pe(L.textContent);te[x]?x+="-"+ ++te[x]:te[x]=1,L.id=x}),Z.body.firstChild.innerHTML};let Ne={content:null,list:[]};e.readmeToc=function(){if(e.type!=="html"||typeof e.content!="string")return[];if(Ne.content===e.content)return Ne.list;let oe=[...new window.DOMParser().parseFromString("<div>"+e.renderReadme(e.sanitize?e.sanitize(e.content):e.content)+"</div>","text/html").querySelectorAll("h2, h3")].map(Z=>({id:Z.id,text:Z.textContent.trim(),level:Z.tagName==="H3"?3:2})).slice(0,40);return Ne={content:e.content,list:oe},oe},e.activeHeading=null,u(window,"scroll",()=>{let U=e.readmeToc?e.readmeToc():[];if(!U.length)return;let oe=150,Z="",te="";for(let L of U){let x=window.document.getElementById(L.id);if(!x)continue;let H=x.getBoundingClientRect().top;H<oe?Z=L.id:!te&&H<window.innerHeight-80&&(te=L.id)}te&&(Z=te),e.activeHeading!==Z&&(e.activeHeading=Z)},{capture:!0,passive:!0}),e.scrollToHeading=function(U){if(!U){(window.document.querySelector(".explorer-content, .file-card-body, .main-pane")||window).scrollTo({top:0,behavior:"smooth"}),e.activeHeading="";return}let oe=window.document.getElementById(U);oe&&oe.scrollIntoView({behavior:"smooth",block:"start"}),e.activeHeading=U},e.redactParts=function(U){let oe=[],Z=0,te,L=/\bXXXX-(\d+)\b/g;for(U=String(U||"");te=L.exec(U);)te.index>Z&&oe.push({t:U.slice(Z,te.index)}),oe.push({n:te[1]}),Z=te.index+te[0].length;return Z<U.length&&oe.push({t:U.slice(Z)}),oe},e.pathCopied=!1,e.copyPath=async function(){window.navigator.clipboard?.writeText&&(await window.navigator.clipboard.writeText(e.filePath||""),e.pathCopied=!0,setTimeout(()=>{e.pathCopied=!1},1600))},e.contentCopied=!1,e.copyContent=async function(){if(typeof e.content!="string"||!window.navigator.clipboard?.writeText)return;let U=e.content;if(e.type==="html"&&e.url)try{U=await(await window.fetch(e.url,{credentials:"same-origin"})).text()}catch{return}await window.navigator.clipboard.writeText(U),e.contentCopied=!0,setTimeout(()=>{e.contentCopied=!1},1600)};function Re(U){return T[U]?T[U]:U}function Qe(U){return U=="pdf"?"pdf":U=="html"||U=="htm"?"html-doc":U=="md"?"md":U=="org"?"org":U=="ipynb"?"IPython":w.indexOf(U)>-1?"text":b.indexOf(U)>-1?"image":N.indexOf(U)>-1?"media":E.indexOf(U)>-1?"audio":"code"}function Ke(U,oe){let Z=p;if(!U){e.type="error",e.content="no_file_selected";return}let te=e.type;(te==="code"||te==="text")&&Ms().catch(()=>{}),e.type="loading",e.content="loading",e.fileLoadingSlow=!1,e.fileReceivedBytes=0;let L=r.defer();v=L,h=a.timeout(()=>{!m&&Z===p&&e.type==="loading"&&(e.fileLoadingSlow=!0)},1500);let x=oe&&oe.sha|| Line truncated
`+w.join(`
`)],{type:"text/csv"}),E=document.createElement("a");E.href=URL.createObjectURL(b),E.download=i.username+"-repositories.csv",E.click()},e.showStatusMessage=h=>{let k=h.statusMessage||"(no message)";window.prompt(`Status message for ${h.repoId} (${h.status}):`,k)},e.fetchGithubInfo=h=>{let k=window.open("","_blank");k&&k.document.write("<pre>Loading GitHub info for "+h.repoId+"...</pre>"),t.get("/api/admin/repos/"+h.repoId+"/github").then(A=>{k&&(k.document.open(),k.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(A.data,null,2).replace(/[<>]/g,w=>w==="<"?"&lt;":"&gt;")+"</pre>"),k.document.close())},A=>{let w=A&&A.data?JSON.stringify(A.data,null,2):String(A);k&&(k.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+w+"</pre>")})};function c(h){t.get("/api/admin/users/"+h+"/repos",{}).then(k=>{e.repositories=k.data},k=>{console.error(k)})}function m(h){t.get("/api/admin/users/"+h,{}).then(k=>{e.userInfo=k.data},k=>{console.error(k)})}m(i.username),c(i.username),e.banUser=()=>{confirm(`Ban user ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/ban`).then(()=>m(i.username),h=>console.error(h))},e.activateUser=()=>{t.post(`/api/admin/users/${i.username}/activate`).then(()=>m(i.username),h=>console.error(h))},e.promoteUser=()=>{confirm(`Promote ${i.username} to admin?`)&&t.post(`/api/admin/users/${i.username}/promote`).then(()=>m(i.username),h=>console.error(h))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/demote`).then(()=>m(i.username),h=>console.error(h))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function g(){t.get("/api/admin/tokens").then(h=>{e.tokens=h.data||[]},h=>{h.status!==401&&h.status!==403&&console.error(h)})}g(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(h=>{e.tokenForm.plaintext=h.data.token,e.tokenForm.name="",g()},h=>console.error(h))},e.revokeToken=h=>{confirm(`Revoke token "${h.name}"?`)&&t.delete("/api/admin/tokens/"+h.id).then(()=>g(),k=>console.error(k))},e.removeCache=h=>{confirm("Remove cached files for "+h.repoId+"?")&&t.delete("/api/admin/repos/"+h.repoId).then(()=>c(i.username),k=>console.error(k))},e.removeRepository=h=>{confirm("Remove repository "+h.repoId+"?")&&t.delete("/api/repo/"+h.repoId+"/").then(()=>c(i.username),k=>console.error(k))},e.updateRepository=h=>{let k=et({title:`Refreshing ${h.repoId}...`,date:new Date,body:`The repository ${h.repoId} is going to be refreshed.`});e.toasts.push(k),t.post(`/api/repo/${h.repoId}/refresh`).then(A=>{A.data.status=="ready"?k.title=`${h.repoId} is refreshed.`:k.title=`Refreshing of ${h.repoId}.`},A=>{k.title=`Error during the refresh of ${h.repoId}.`,k.body=A.body})},e.getGitHubRepositories=h=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(k=>{e.userInfo.repositories=k.data})};let v=null;e.watch("query",()=>{o.timeout.cancel(v),v=o.timeout(()=>{c(i.username)},500)},!0)},sc=function(e,t,s){let i=At(),o=vn();e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let r=A=>{if(A.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){A.preventDefault();let w=document.querySelector('.admin-filter-toolbar input[type="search"]');w&&w.focus()}};o(document,"keydown",r),e.on("dispose",()=>document.removeEventListener("keydown",r)),e.clearFilter=A=>{A==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[A]="",e.query.page=1},e.chips=[];let a=()=>{let A=[];(e.query.dateFrom||e.query.dateTo)&&A.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=A};e.statusCountFor=A=>{let w=(e.statusCounts||[]).find(b=>b._id===A);return w?w.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=A=>{e.query.sort===A?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=A,e.query.direction="desc"),e.query.page=1},e.sortIcon=A=>e.query.sort===A?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",c={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!0,expired:!0,removed:!1,error:!0,preparing:!0},m=loadFilterPrefs(u)||{};e.query=Object.assign({},c,m,{page:1,search:""});let g=s.search();g.search&&(e.query.search=g.search);let v="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(v)||"[]"),e.savePreset=()=>{let A=window.prompt("Preset name:");if(!A)return;let w=Object.assign({},e.query);delete w.page,e.presets=(e.presets||[]).filter(b=>b.name!==A),e.presets.push({name:A,query:w}),localStorage.setItem(v,JSON.stringify(e.presets))},e.applyPreset=A=>{Object.assign(e.query,A.query,{page:1})},e.deletePreset=A=>{e.presets=(e.presets||[]).filter(w=>w.name!==A.name),localStorage.setItem(v,JS Line truncated
`).map((_e,Ne)=>Ne===0?_e:ke+_e).join(`
`)}else typeof D=="number"||typeof D=="boolean"?me=String(D):me=JSON.stringify(D);de.push(`${Ee}${me}${re}`)}),de.push("}"),de.join(`
`)}function m(E){let N=[],O="",q=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,I=0,j;for(;j=q.exec(E);)O+=E.slice(I,j.index),I=q.lastIndex,N.push({key:j[1],op:j[2]||"=",val:j[3]});return O+=E.slice(I),{filters:N,free:O.trim().toLowerCase()}}function g(E,N){for(let O of N.filters){let q=(j,de,ie)=>{let D=parseFloat(j),X=parseFloat(de);return ie==="="?String(j)===String(de):ie==="!="?String(j)!==String(de):ie===">="?D>=X:ie==="<="?D<=X:ie===">"?D>X:ie==="<"?D<X:!0},I;if(O.key==="code")I=E.displayMessage;else if(O.key==="module")I=E.module;else if(O.key==="status")I=E._status;else if(O.key==="url")I=E._url;else if(O.key==="repo")I=E._repoId;else if(O.key==="level")I=E.level;else continue;if(I==null||!q(I,O.val,O.op))return!1}return!(N.free&&((E.displayMessage||"")+" "+(E.module||"")+" "+(E._url||"")+" "+JSON.stringify(E.raw||[])).toLowerCase().indexOf(N.free)===-1)}function v(){let E=m(e.query.search||"");e.parsedFilterCount=E.filters.length;let N=e.query.bucket,O=e.entries.filter(ie=>N&&ie._bucket!==N?!1:g(ie,E)),q=e.query.group;if(q){let ie=le=>q==="module"?le.module:le.displayMessage||le.message||"_",D=new Map;for(let le of O){let Ee=ie(le);if(D.has(Ee)){let re=D.get(Ee);re.count++,re._related.push(le),new Date(le.ts)>new Date(re.ts)&&(re.ts=le.ts,re._url=le._url,re._status=le._status),new Date(le.ts)<new Date(re._firstSeen)&&(re._firstSeen=le.ts)}else{let re=Object.assign({},le);re._key=`${q}:${Ee}`,re._related=[le],re._firstSeen=le.ts,re._lastHourCount=0,re.count=1,D.set(Ee,re)}}let X=Date.now()-3600*1e3;for(let le of D.values())le._lastHourCount=le._related.filter(Ee=>new Date(Ee.ts).getTime()>=X).length;O=Array.from(D.values())}else O=O.map((ie,D)=>(ie._key="row:"+D+":"+ie.ts,ie._related=[ie],ie._firstSeen=ie.ts,ie._lastHourCount=0,ie.count=1,ie));let I=ie=>({error:0,warn:1,info:2})[ie._bucket]??1;e.query.sort==="count"?O.sort((ie,D)=>I(ie)-I(D)||D.count-ie.count||new Date(D.ts)-new Date(ie.ts)):O.sort((ie,D)=>I(ie)-I(D)||new Date(D.ts)-new Date(ie.ts)),e.visible=O;let j=!!(e.query.bucket||(e.query.search||"").trim()),de=!j&&(e.stats?.severity?.error||0)>0&&!O.some(ie=>ie._bucket==="error")&&e.entries.length<1500;(j&&!O.length||de)&&e.canLoadMore?.()&&!e._autoLoading&&e.entries.length<2500&&(e._autoLoading=!0,h(!0))}function h(E){let N=E?e.entries.length:0,O=E?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:N,limit:O}}).then(q=>{let I=(q.data.entries||[]).map(u);e.entries=E?e.entries.concat(I):I,e.available=!!q.data.available,e.cap=q.data.max||e.cap,e.total=q.data.total||e.entries.length,e._autoLoading=!1,v()},q=>{e._autoLoading=!1,console.error(q)})}e.loadMore=()=>h(!0),e.canLoadMore=()=>e.entries.length<e.total;function k(){t.get("/api/admin/errors/stats").then(E=>{let N=E.data||{},O=N.prev24h?Math.round((N.last24h-N.prev24h)/N.prev24h*100):0;e.stats={last24h:N.last24h||0,prev24h:N.prev24h||0,delta:O,severity:N.severity||{error:0,warn:0,info:0},unique:N.unique||{error:0,warn:0,info:0},buckets:N.buckets||[],dropped:N.dropped||0},v()},E=>console.error(E))}function A(){h(),k()}e.barPx=(E,N)=>{let O=e.stats.buckets||[],q=0,I=0;for(let D of O)q=Math.max(q,(D.error||0)+(D.warn||0)),I=Math.max(I,D.info||0);let j=E[N]||0;if(!j)return 0;if(N==="info")return I?Math.max(2,Math.round(j/I*(q?16:60))):0;let de=(E.error||0)+(E.warn||0),ie=Math.round(de/q*44);return Math.max(4,Math.round(j/de*ie))},e.bucketTitle=E=>`${new Date(E.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${E.error||0} err \xB7 ${E.warn||0} warn \xB7 ${E.info||0} info`,e.toggle=E=>{e.expanded[E._key]=!e.expanded[E._key]},e.setBucket=E=>{e.query.bucket=E},e.refreshNow=A,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(A,E=>console.error(E))},e.exportCsv=()=>{let E=["ts","level","module","displayMessage","_status","_url","_repoId"],N=[E.join(",")];for(let j of e.visible)N.push(E.map(de=>{let ie=j[de]==null?"":String(j[de]);return/[",\n]/.test(ie)?`"${ie.replace(/"/g,'""')}"`:ie}).join(","));let O=new Blob([N.join(`
`)],{type:"text/csv;charset=utf-8"}),q=URL.createObjectURL(O),I=document.createElement("a");I.href=q,I.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(I),I.click(),document.body.removeChild(I),URL.revokeObjectURL(q)};function w(E){e.copyHint=`${E} copied`,o.timeout(()=>{e.copyHint=""},1500)}e.copyJson=E=>{navigator.clipboard.writeText(E._detailJson).then(()=>w("JSON"))},e.copyCurl=E=>{if(!E._url)return;let O=`curl -X ${E._method||"GET"} '${window.location.origin}${E._url}'`;navigator.clipboard.writeText(O).then(()=>w("curl"))},A();let b=i(()=>{e.query.autoRefresh&&A()},15e3);e.on("dispose",()=>i.cancel(b)),e.watch("query.search",v),e.watch("query.bucket",v),e.watch("query.sort",v),e.watch("query.group",v)},rc=function(e,t,s,i){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null){s.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceTab="routes",e.cacheReport=null,e.cacheError=null,e.cacheMinutes=60,e.cacheTraffic="all",e.ovView=["performance","cache"].includes((window.location.hash||"").slice(1))?window.location.hash.slice(1):"summary",e.setOvView=function(b){e.ovView=b;try{window.history.replaceState(null,"",b==="summary"?window.location.pathname:"#"+b)}catch{}};var o=!1;e.loadCache=function(){if(!o){o=!0;var b=e.cacheMinutes;t.get("/api/admin/cache",{params:{minutes:b}}).then(function(E){b===e.cacheMinutes&&(e.cacheReport=E.data,e.cacheError=E.data.available?null:E.data.reason||"Cache monitoring is unavailable.")},function(){e.cacheError="Could not refresh cache monitoring. Displayed values may be stale."}).finally(function(){o=!1,b!==e.cacheMinutes&&e.loadCache()})}},e.cacheRows=function(){return(e.cacheReport?.rows||[]).filter(function(b){return e.cacheTraffic==="all"||b.traffic===e.cacheTraffic})},e.cacheRate=function(b,E){var N=e.cacheRows(),O=function(j){return N.filter(function(de){return de.name===j}).reduce(function(de,ie){return de+ie[E]},0)},q=O(b+"_hit"),I=O(b+"_miss");return q+I?(100*q/(q+I)).toFixed(1)+"%":"No samples"},e.cacheEventName=function(b){return b.replaceAll("_"," ")},e.cacheP95=function(b){var E=0,N=e.cacheReport?.latencyBucketsMs||[];if(!b.count)return"No samples";if(!b.ms)return"Not timed";for(var O=0;O<=N.length;O++)if(E+=b["b"+O]||0,E>=Math.ceil(b.count*.95))return O<N.length?"\u2264 "+N[O]+" ms":"> 10 s";return"No samples"},e.performanceError=null,e.formatMs=function(b){return b==null?"":b<1e3?b+" ms":+(b/1e3).toFixed(1)+" s"},e.latencyBound=function(b,E){return b.count?b[E]==null?"> 60 s":"\u2264 "+e.formatMs(b[E]):"No samples"},e.firstByteBound=function(b){return e.latencyBound({count:b.firstByteCount,p95UpperMs:b.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(b){return Math.max(0,Math.round((Date.now()-b.sampledAt)/1e3))},e.peakRss=function(b){return(e.performance.runtimeSeries||[]).filter(function(E){return E.instance===b.instance}).reduce(function(E,N){return Math.max(E,N.rss)},b.memory.rss)},e.performanceWarning=function(b){return e.sampleAge(b)>45||b.droppedBatches||b.droppedMetrics||b.sockets?.closeWait>0||b.eventLoop?.p95Ms>100||b.memoryLimitBytes&&b.memory.rss>b.memoryLimitBytes*.8};var r=!1;e.loadPerformance=function(){var b=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==b&&(e.performance=null),!r&&(r=!0,t.get("/api/admin/performance",{params:{minutes:b}}).then(function(E){b===e.performanceMinutes&&(e.performance=E.data,e.performanceError=E.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){b===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){r=!1,b!==e.performanceMinutes&&e.loadPerformance()}))};function a(b){if(b==null)return"\u2014";for(var E=["B","KB","MB","GB","TB"],N=0,O=b;O>=1024&&N<E.length-1;)O/=1024,N++;return O.toFixed(N>0?1:0)+" "+E[N]}e.humanBytes=a;function u(b){if(!b)return"\u2014";var E=Math.floor(b/86400),N=Math.floor(b%86400/3600),O=Math.floor(b%3600/60);return E>0?E+"d "+(N<10?"0":"")+N+"h":N>0?N+"h "+(O<10?"0":"")+O+"m":O+"m"}e.humanDuration=u;function c(b){return b==null?"\u2014":b>=1e6?(b/1e6).toFixed(1)+"M":b>=1e3?(b/1e3).toFixed(1)+"K":String(b)}e.humanNum=c,e.queueTotal=function(b){return b?(b.waiting||0)+(b.active||0)+(b.delayed||0)+(b.failed||0):0},e.statusCount=function(b){if(!e.data||!e.data.repos)return 0;for(var E=e.data.repos.statusBreakdown||[],N=0;N<E.length;N++)if(E[N]._id===b)return E[N].count;return 0},e.barPct=function(b){var E=e.data&&e.data.repos?e.data.repos.total:0;if(!E)return 0;var N=[b];b==="expired"&&N.push("expiring"),b==="removed"&&N.push("removing"),b==="preparing"&&N.push("download");var O=0;return N.forEach(function(q){O+=e.statusCount(q)}),Math.max(.4,O/E*100)},e.errPct=function(b){if(!e.data||!e.data.errors)return 0;var E=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.erro Line truncated
`)],{type:"text/csv"}),E=document.createElement("a");E.href=URL.createObjectURL(b),E.download=i.username+"-repositories.csv",E.click()},e.showStatusMessage=m=>{let k=m.statusMessage||"(no message)";window.prompt(`Status message for ${m.repoId} (${m.status}):`,k)},e.fetchGithubInfo=m=>{let k=window.open("","_blank");k&&k.document.write("<pre>Loading GitHub info for "+m.repoId+"...</pre>"),t.get("/api/admin/repos/"+m.repoId+"/github").then(T=>{k&&(k.document.open(),k.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(T.data,null,2).replace(/[<>]/g,w=>w==="<"?"&lt;":"&gt;")+"</pre>"),k.document.close())},T=>{let w=T&&T.data?JSON.stringify(T.data,null,2):String(T);k&&(k.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+w+"</pre>")})};function p(m){t.get("/api/admin/users/"+m+"/repos",{}).then(k=>{e.repositories=k.data},k=>{console.error(k)})}function v(m){t.get("/api/admin/users/"+m,{}).then(k=>{e.userInfo=k.data},k=>{console.error(k)})}v(i.username),p(i.username),e.banUser=()=>{confirm(`Ban user ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/ban`).then(()=>v(i.username),m=>console.error(m))},e.activateUser=()=>{t.post(`/api/admin/users/${i.username}/activate`).then(()=>v(i.username),m=>console.error(m))},e.promoteUser=()=>{confirm(`Promote ${i.username} to admin?`)&&t.post(`/api/admin/users/${i.username}/promote`).then(()=>v(i.username),m=>console.error(m))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/demote`).then(()=>v(i.username),m=>console.error(m))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function h(){t.get("/api/admin/tokens").then(m=>{e.tokens=m.data||[]},m=>{m.status!==401&&m.status!==403&&console.error(m)})}h(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(m=>{e.tokenForm.plaintext=m.data.token,e.tokenForm.name="",h()},m=>console.error(m))},e.revokeToken=m=>{confirm(`Revoke token "${m.name}"?`)&&t.delete("/api/admin/tokens/"+m.id).then(()=>h(),k=>console.error(k))},e.removeCache=m=>{confirm("Remove cached files for "+m.repoId+"?")&&t.delete("/api/admin/repos/"+m.repoId).then(()=>p(i.username),k=>console.error(k))},e.removeRepository=m=>{confirm("Remove repository "+m.repoId+"?")&&t.delete("/api/repo/"+m.repoId+"/").then(()=>p(i.username),k=>console.error(k))},e.updateRepository=m=>{let k=et({title:`Refreshing ${m.repoId}...`,date:new Date,body:`The repository ${m.repoId} is going to be refreshed.`});e.toasts.push(k),t.post(`/api/repo/${m.repoId}/refresh`).then(T=>{T.data.status=="ready"?k.title=`${m.repoId} is refreshed.`:k.title=`Refreshing of ${m.repoId}.`},T=>{k.title=`Error during the refresh of ${m.repoId}.`,k.body=T.body})},e.getGitHubRepositories=m=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(k=>{e.userInfo.repositories=k.data})};let g=null;e.watch("query",()=>{s.timeout.cancel(g),g=s.timeout(()=>{p(i.username)},500)},!0)},ic=function(e,t,o){let i=Tt(),s=bn();e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let r=T=>{if(T.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){T.preventDefault();let w=document.querySelector('.admin-filter-toolbar input[type="search"]');w&&w.focus()}};s(document,"keydown",r),e.on("dispose",()=>document.removeEventListener("keydown",r)),e.clearFilter=T=>{T==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[T]="",e.query.page=1},e.chips=[];let a=()=>{let T=[];(e.query.dateFrom||e.query.dateTo)&&T.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=T};e.statusCountFor=T=>{let w=(e.statusCounts||[]).find(b=>b._id===T);return w?w.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=T=>{e.query.sort===T?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=T,e.query.direction="desc"),e.query.page=1},e.sortIcon=T=>e.query.sort===T?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",p={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!0,expired:!0,removed:!1,error:!0,preparing:!0},v=loadFilterPrefs(u)||{};e.query=Object.assign({},p,v,{page:1,search:""});let h=o.search();h.search&&(e.query.search=h.search);let g="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(g)||"[]"),e.savePreset=()=>{let T=window.prompt("Preset name:");if(!T)return;let w=Object.assign({},e.query);delete w.page,e.presets=(e.presets||[]).filter(b=>b.name!==T),e.presets.push({name:T,query:w}),localStorage.setItem(g,JSON.stringify(e.presets))},e.applyPreset=T=>{Object.assign(e.query,T.query,{page:1})},e.deletePreset=T=>{e.presets=(e.presets||[]).filter(w=>w.name!==T.name),localStorage.setItem(g,JS Line truncated
`).map((Pe,Ne)=>Ne===0?Pe:Ce+Pe).join(`
`)}else typeof D=="number"||typeof D=="boolean"?he=String(D):he=JSON.stringify(D);de.push(`${Ee}${he}${ae}`)}),de.push("}"),de.join(`
`)}function v(E){let N=[],O="",q=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,M=0,G;for(;G=q.exec(E);)O+=E.slice(M,G.index),M=q.lastIndex,N.push({key:G[1],op:G[2]||"=",val:G[3]});return O+=E.slice(M),{filters:N,free:O.trim().toLowerCase()}}function h(E,N){for(let O of N.filters){let q=(G,de,re)=>{let D=parseFloat(G),X=parseFloat(de);return re==="="?String(G)===String(de):re==="!="?String(G)!==String(de):re===">="?D>=X:re==="<="?D<=X:re===">"?D>X:re==="<"?D<X:!0},M;if(O.key==="code")M=E.displayMessage;else if(O.key==="module")M=E.module;else if(O.key==="status")M=E._status;else if(O.key==="url")M=E._url;else if(O.key==="repo")M=E._repoId;else if(O.key==="level")M=E.level;else continue;if(M==null||!q(M,O.val,O.op))return!1}return!(N.free&&((E.displayMessage||"")+" "+(E.module||"")+" "+(E._url||"")+" "+JSON.stringify(E.raw||[])).toLowerCase().indexOf(N.free)===-1)}function g(){let E=v(e.query.search||"");e.parsedFilterCount=E.filters.length;let N=e.query.bucket,O=e.entries.filter(re=>N&&re._bucket!==N?!1:h(re,E)),q=e.query.group;if(q){let re=le=>q==="module"?le.module:le.displayMessage||le.message||"_",D=new Map;for(let le of O){let Ee=re(le);if(D.has(Ee)){let ae=D.get(Ee);ae.count++,ae._related.push(le),new Date(le.ts)>new Date(ae.ts)&&(ae.ts=le.ts,ae._url=le._url,ae._status=le._status),new Date(le.ts)<new Date(ae._firstSeen)&&(ae._firstSeen=le.ts)}else{let ae=Object.assign({},le);ae._key=`${q}:${Ee}`,ae._related=[le],ae._firstSeen=le.ts,ae._lastHourCount=0,ae.count=1,D.set(Ee,ae)}}let X=Date.now()-3600*1e3;for(let le of D.values())le._lastHourCount=le._related.filter(Ee=>new Date(Ee.ts).getTime()>=X).length;O=Array.from(D.values())}else O=O.map((re,D)=>(re._key="row:"+D+":"+re.ts,re._related=[re],re._firstSeen=re.ts,re._lastHourCount=0,re.count=1,re));let M=re=>({error:0,warn:1,info:2})[re._bucket]??1;e.query.sort==="count"?O.sort((re,D)=>M(re)-M(D)||D.count-re.count||new Date(D.ts)-new Date(re.ts)):O.sort((re,D)=>M(re)-M(D)||new Date(D.ts)-new Date(re.ts)),e.visible=O;let G=!!(e.query.bucket||(e.query.search||"").trim()),de=!G&&(e.stats?.severity?.error||0)>0&&!O.some(re=>re._bucket==="error")&&e.entries.length<1500;(G&&!O.length||de)&&e.canLoadMore?.()&&!e._autoLoading&&e.entries.length<2500&&(e._autoLoading=!0,m(!0))}function m(E){let N=E?e.entries.length:0,O=E?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:N,limit:O}}).then(q=>{let M=(q.data.entries||[]).map(u);e.entries=E?e.entries.concat(M):M,e.available=!!q.data.available,e.cap=q.data.max||e.cap,e.total=q.data.total||e.entries.length,e._autoLoading=!1,g()},q=>{e._autoLoading=!1,console.error(q)})}e.loadMore=()=>m(!0),e.canLoadMore=()=>e.entries.length<e.total;function k(){t.get("/api/admin/errors/stats").then(E=>{let N=E.data||{},O=N.prev24h?Math.round((N.last24h-N.prev24h)/N.prev24h*100):0;e.stats={last24h:N.last24h||0,prev24h:N.prev24h||0,delta:O,severity:N.severity||{error:0,warn:0,info:0},unique:N.unique||{error:0,warn:0,info:0},buckets:N.buckets||[],dropped:N.dropped||0},g()},E=>console.error(E))}function T(){m(),k()}e.barPx=(E,N)=>{let O=e.stats.buckets||[],q=0,M=0;for(let D of O)q=Math.max(q,(D.error||0)+(D.warn||0)),M=Math.max(M,D.info||0);let G=E[N]||0;if(!G)return 0;if(N==="info")return M?Math.max(2,Math.round(G/M*(q?16:60))):0;let de=(E.error||0)+(E.warn||0),re=Math.round(de/q*44);return Math.max(4,Math.round(G/de*re))},e.bucketTitle=E=>`${new Date(E.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${E.error||0} err \xB7 ${E.warn||0} warn \xB7 ${E.info||0} info`,e.toggle=E=>{e.expanded[E._key]=!e.expanded[E._key]},e.setBucket=E=>{e.query.bucket=E},e.refreshNow=T,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(T,E=>console.error(E))},e.exportCsv=()=>{let E=["ts","level","module","displayMessage","_status","_url","_repoId"],N=[E.join(",")];for(let G of e.visible)N.push(E.map(de=>{let re=G[de]==null?"":String(G[de]);return/[",\n]/.test(re)?`"${re.replace(/"/g,'""')}"`:re}).join(","));let O=new Blob([N.join(`
`)],{type:"text/csv;charset=utf-8"}),q=URL.createObjectURL(O),M=document.createElement("a");M.href=q,M.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(M),M.click(),document.body.removeChild(M),URL.revokeObjectURL(q)};function w(E){e.copyHint=`${E} copied`,s.timeout(()=>{e.copyHint=""},1500)}e.copyJson=E=>{navigator.clipboard.writeText(E._detailJson).then(()=>w("JSON"))},e.copyCurl=E=>{if(!E._url)return;let O=`curl -X ${E._method||"GET"} '${window.location.origin}${E._url}'`;navigator.clipboard.writeText(O).then(()=>w("curl"))},T();let b=i(()=>{e.query.autoRefresh&&T()},15e3);e.on("dispose",()=>i.cancel(b)),e.watch("query.search",g),e.watch("query.bucket",g),e.watch("query.sort",g),e.watch("query.group",g)},lc=function(e,t,o,i){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceTab="routes",e.cacheReport=null,e.cacheError=null,e.cacheMinutes=60,e.cacheTraffic="all",e.ovView=["performance","cache"].includes((window.location.hash||"").slice(1))?window.location.hash.slice(1):"summary",e.setOvView=function(b){e.ovView=b;try{window.history.replaceState(null,"",b==="summary"?window.location.pathname:"#"+b)}catch{}};var s=!1;e.loadCache=function(){if(!s){s=!0;var b=e.cacheMinutes;t.get("/api/admin/cache",{params:{minutes:b}}).then(function(E){b===e.cacheMinutes&&(e.cacheReport=E.data,e.cacheError=E.data.available?null:E.data.reason||"Cache monitoring is unavailable.")},function(){e.cacheError="Could not refresh cache monitoring. Displayed values may be stale."}).finally(function(){s=!1,b!==e.cacheMinutes&&e.loadCache()})}},e.cacheRows=function(){return(e.cacheReport?.rows||[]).filter(function(b){return e.cacheTraffic==="all"||b.traffic===e.cacheTraffic})},e.cacheRate=function(b,E){var N=e.cacheRows(),O=function(G){return N.filter(function(de){return de.name===G}).reduce(function(de,re){return de+re[E]},0)},q=O(b+"_hit"),M=O(b+"_miss");return q+M?(100*q/(q+M)).toFixed(1)+"%":"No samples"},e.cacheEventName=function(b){return b.replaceAll("_"," ")},e.cacheP95=function(b){var E=0,N=e.cacheReport?.latencyBucketsMs||[];if(!b.count)return"No samples";if(!b.ms)return"Not timed";for(var O=0;O<=N.length;O++)if(E+=b["b"+O]||0,E>=Math.ceil(b.count*.95))return O<N.length?"\u2264 "+N[O]+" ms":"> 10 s";return"No samples"},e.performanceError=null,e.formatMs=function(b){return b==null?"":b<1e3?b+" ms":+(b/1e3).toFixed(1)+" s"},e.latencyBound=function(b,E){return b.count?b[E]==null?"> 60 s":"\u2264 "+e.formatMs(b[E]):"No samples"},e.firstByteBound=function(b){return e.latencyBound({count:b.firstByteCount,p95UpperMs:b.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(b){return Math.max(0,Math.round((Date.now()-b.sampledAt)/1e3))},e.peakRss=function(b){return(e.performance.runtimeSeries||[]).filter(function(E){return E.instance===b.instance}).reduce(function(E,N){return Math.max(E,N.rss)},b.memory.rss)},e.performanceWarning=function(b){return e.sampleAge(b)>45||b.droppedBatches||b.droppedMetrics||b.sockets?.closeWait>0||b.eventLoop?.p95Ms>100||b.memoryLimitBytes&&b.memory.rss>b.memoryLimitBytes*.8};var r=!1;e.loadPerformance=function(){var b=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==b&&(e.performance=null),!r&&(r=!0,t.get("/api/admin/performance",{params:{minutes:b}}).then(function(E){b===e.performanceMinutes&&(e.performance=E.data,e.performanceError=E.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){b===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){r=!1,b!==e.performanceMinutes&&e.loadPerformance()}))};function a(b){if(b==null)return"\u2014";for(var E=["B","KB","MB","GB","TB"],N=0,O=b;O>=1024&&N<E.length-1;)O/=1024,N++;return O.toFixed(N>0?1:0)+" "+E[N]}e.humanBytes=a;function u(b){if(!b)return"\u2014";var E=Math.floor(b/86400),N=Math.floor(b%86400/3600),O=Math.floor(b%3600/60);return E>0?E+"d "+(N<10?"0":"")+N+"h":N>0?N+"h "+(O<10?"0":"")+O+"m":O+"m"}e.humanDuration=u;function p(b){return b==null?"\u2014":b>=1e6?(b/1e6).toFixed(1)+"M":b>=1e3?(b/1e3).toFixed(1)+"K":String(b)}e.humanNum=p,e.queueTotal=function(b){return b?(b.waiting||0)+(b.active||0)+(b.delayed||0)+(b.failed||0):0},e.statusCount=function(b){if(!e.data||!e.data.repos)return 0;for(var E=e.data.repos.statusBreakdown||[],N=0;N<E.length;N++)if(E[N]._id===b)return E[N].count;return 0},e.barPct=function(b){var E=e.data&&e.data.repos?e.data.repos.total:0;if(!E)return 0;var N=[b];b==="expired"&&N.push("expiring"),b==="removed"&&N.push("removing"),b==="preparing"&&N.push("download");var O=0;return N.forEach(function(q){O+=e.statusCount(q)}),Math.max(.4,O/E*100)},e.errPct=function(b){if(!e.data||!e.data.errors)return 0;var E=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.erro Line truncated
`).length+1)),placeholder:`Jane Smith
MIT CSAIL
jane.smith@example.org`},null,10,AT),[[o,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"anonymize",options:{debounce:250}}]]),t[94]||(t[94]=n("small",{class:"form-text text-muted"},"One term per line.",-1)),n("details",_T,[t[89]||(t[89]=n("summary",null,"Regex & replacement syntax",-1)),t[90]||(t[90]=y()),n("small",OT,[t[83]||(t[83]=y("Regex is supported. Replaced by ",-1)),n("code",null,p(e.site_options?.ANONYMIZATION_MASK)+"-[N]",1),t[84]||(t[84]=y(", or use ",-1)),t[85]||(t[85]=n("code",null,"term=>replacement",-1)),t[86]||(t[86]=y(" to pick your own (e.g. ",-1)),t[87]||(t[87]=n("code",null,"Anonymous=>ABC",-1)),t[88]||(t[88]=y(").",-1))])]),(l(!0),d(W,null,ae((e.termHints||[]).slice(0,3),a=>(l(),d("div",{class:"term-hint",key:a.line},[n("span",null,[t[91]||(t[91]=y("In ",-1)),n("code",null,p(a.term),1),t[92]||(t[92]=y(", ",-1)),n("code",null,p(a.symbols.join(" ")),1),y(" "+p(a.symbols.length>1?"are regex symbols":"is a regex symbol")+", so it may not match the text as written.",1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm",onClick:u=>e.matchTermAsWritten(a.line)},"Match as written",8,PT)]))),128)),(e.termHints||[]).length>3?(l(),d("div",IT,p(e.termHints.length-3)+" more "+p(e.termHints.length-3===1?"term uses":"terms use")+" regex symbols.",1)):f("v-if",!0),T(n("div",MT,"Terms are in an invalid format.",512),[[J,e.anonymize?.terms?.errors?.format]])]),T(n("div",VT,[n("div",qT,[t[95]||(t[95]=n("span",{class:"paper-field-label",id:"preserve-files-label"},"Unredacted files",-1)),n("span",$T,p(e.preserveSummary()),1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm","aria-describedby":"preserve-files-label",disabled:!e.source?.commit,onClick:t[19]||(t[19]=a=>e.openPreserveFiles())},p((e.options?.preserveFiles||[]).length?"Edit":"Choose files"),9,LT)],512),[[J,e.detectedType==="repo"]]),e.preserve?.open?(l(),d("dialog",{key:0,id:"preserve-files-dialog",class:"preserve-dialog","aria-labelledby":"preserve-files-title",onCancel:t[24]||(t[24]=ce(a=>e.closePreserveFiles(),["prevent"]))},[n("form",{onSubmit:t[23]||(t[23]=ce(a=>e.savePreserveFiles(),["prevent"]))},[t[99]||(t[99]=n("h2",{id:"preserve-files-title"},"Leave files unredacted",-1)),t[100]||(t[100]=n("p",{class:"preserve-intro"},"Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.",-1)),T(n("input",{type:"search",id:"preserve-files-search",class:"form-control",placeholder:"Search files","aria-label":"Search files",autocomplete:"off","onUpdate:modelValue":t[20]||(t[20]=a=>e.preserve.search=a)},null,512),[[dt,e.preserve.search]]),n("div",UT,[e.preserve.loading?(l(),d("p",FT,"Loading files from GitHub\u2026")):e.preserve.error?(l(),d("p",BT,p(e.preserve.error),1)):(l(),d(W,{key:2},[(l(!0),d(W,null,ae(e.preserveRows(),(a,u)=>(l(),d("div",{class:"preserve-item",key:a.kind+":"+a.path,style:$e({paddingLeft:a.depth*18+6+"px"})},[a.kind==="dir"?(l(),d("button",{key:0,type:"button",class:"preserve-caret","aria-expanded":a.open?"true":"false","aria-label":(a.open?"Collapse ":"Expand ")+a.label,onClick:c=>e.togglePreserveDir(a.path)},p(a.open?"\u25BE":"\u25B8"),9,zT)):(l(),d("span",HT)),n("input",{type:"checkbox",id:"pf-"+u,"aria-label":a.kind==="dir"?"Leave folder "+a.path+" unredacted":null,checked:a.state==="all",".indeterminate":a.state==="some",disabled:a.locked,onChange:c=>a.kind==="dir"?e.togglePreserveFolder(a.path):e.togglePreserveFile(a.path)},null,40,jT),n("label",{for:a.kind==="file"?"pf-"+u:null,class:"preserve-name",onClick:c=>a.kind==="dir"&&e.togglePreserveDir(a.path)},p(a.label),9,GT),a.note?(l(),d("span",WT,p(a.note),1)):f("v-if",!0)],4))),128)),!e.preserve.loading&&!e.preserveRows().length?(l(),d("p",KT,p(e.preserve.search?"No file matches your search.":"No text files in this commit."),1)):f("v-if",!0)],64))]),e.preserve.truncated?(l(),d("p",YT,"This repository has too many files to list them all. Use a pattern for files you cannot find.")):f("v-if",!0),n("details",{class:"preserve-patterns",open:e.preserve.patternsOpen},[t[96]||(t[96]=n("summary",null,"Patterns",-1)),T(n("textarea",{class:"form-control",rows:"3","aria-label":"Patterns",placeholder:`*.bib
paper/appendix/**`,"onUpdate:modelValue":t[21]||(t[21]=a=>e.preserve.patterns=a)},null,512),[[dt,e.preserve.patterns]]),t[97]||(t[97]=n("small",{class:"form-text text-muted"},[y("One per line, as in .gitignore: "),n("code",null,"*.bib"),y(" matches in any folder; "),n("code",null,"/refs.bib"),y(" and "),n("code",null,"paper/refs.bib"),y(" match from the repository root; a folder name such as "),n("code",null,"drafts/"),y(" covers everything inside it; "),n("code",null,"!private.bib"),y(" excludes again.")],-1))],8,XT),n("div",JT,[n("span",QT,p(e.preserve.loading||e.preserve.error?"":e.preserveCount()+(e.preserveCount()===1?" file":" files")+" will be shown as written"),1),n("span",ZT,[n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[22]||(t[22]=a=>e.closePreserveFiles())},"Cancel"),t[98]||(t[98]=n("button",{type:"submit",class:"btn btn-ink"},"Save",-1))])])],32)],32)):f("v-if",!0)],512),[[J,e.detectedType]]),T(n("section",xT,[t[121]||(t[121]=n("h2",{class:"paper-section-eyebrow"},"Display",-1)),T(n("div",null,[n("div",eA,[T(n("input",tA,null,512),[[o,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"anonymize",options:{}}]]),t[102]||(t[102]=n("label",{class:"form-check-label",for:"link"},"Keep links",-1))]),n("div",nA,[T(n("input",sA,null,512),[[o,{state:e.viewState,set:a=>{e.options.image=a},value:e.options?.image,form:"anonymize",options:{}}]]),t[103]||(t[103]=n("label",{class:"form-check-label",for:"image"},"Display images",-1))]),n("div",oA,[T(n("input",iA,null,512),[[o,{state:e.viewState,set:a=>{e.options.pdf=a},value:e.options?.pdf,form:"anonymize",options:{}}]]),t[104]||(t[104]=n("label",{class:"form-check-label",for:"pdf"},"Display PDFs",-1))]),n("div",rA,[T(n("input",aA,null,512),[[o,{state:e.viewState,set:a=>{e.options.notebook=a},value:e.options?.notebook,form:"anonymize",options:{}}]]),t[105]||(t[105]=n("label",{class:"form-check-label",for:"notebook"},"Display Notebooks",-1))]),n("div",lA,[T(n("input",{class:"form-check-input",type:"checkbox",id:"page",name:"page",disabled:!e.details?.hasPage||e.details?.pageSource&&e.details?.pageSource?.branch!==e.source?.branch},null,8,dA),[[o,{state:e.viewState,set:a=>{e.options.page=a},value:e.options?.page,form:"anonymize",options:{}}]]),t[106]||(t[106]=n("label",{class:"form-check-label",for:"page"},"GitHub Pages",-1)),e.details?.hasPage?f("v-if",!0):(l(),d("small",uA,p(e.fmt?.translate("WARNINGS.page_not_enabled_on_repo")),1)),e.details?.hasPage&&e.details?.pageSource?.branch&&e.source?.branch&&e.details.pageSource.branch!==e.source.branch?(l(),d("small",cA,p(e.fmt?.translate("WARNINGS.page_branch_mismatch",{pageBranch:e.details?.pageSource?.branch,selectedBranch:e.source?.branch})),1)):f("v-if",!0)])],512),[[J,e.detectedType==="repo"]]),e.detectedType==="repo"&&(e.options.image||e.options.pdf)?(l(),d("p",pA,[...t[107]||(t[107]=[y("Images and PDFs are served unchanged. Review their contents and metadata before sharing. ",-1),n("a",{href:"/faq#formats"},"Supported formats",-1)])])):f("v-if",!0),T(n("div",null,[n("div",fA,[T(n("input",hA,null,512),[[o,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[108]||(t[108]=n("label",{class:"form-check-label",for:"title-gist"},"Gist description",-1))]),n("div",mA,[T(n("input",gA,null,512),[[o,{state:e.viewState,set:a=>{e.options.content=a},value:e.options?.content,form:"anonymize",options:{}}]]),t[109]||(t[109]=n("label",{class:"form-check-label",for:"content-gist"},"File contents",-1))]),n("div",vA,[T(n("input",yA,null,512),[[o,{state:e.viewState,set:a=>{e.options.comments=a},value:e.options?.comments,form:"anonymize",options:{}}]]),t[110]||(t[110]=n("label",{class:"form-check-label",for:"comments-gist"},"Comments",-1))]),n("div",bA,[T(n("input",wA,null,512),[[o,{state:e.viewState,set:a=>{e.options.username=a},value:e.options?.username,form:"anonymize",options:{}}]]),t[111]||(t[111]=n("label",{class:"form-check-label",for:"username-gist"},"Usernames",-1))]),n("div",kA,[T(n("input",CA,null,512),[[o,{state:e.viewState,set:a=>{e.options.date=a},value:e.options?.date,form:"anonymize",options:{}}]]),t[112]||(t[112]=n("label",{class:"form-check-label",for:"date-gist"},"Dates",-1))]),n("div",EA,[T(n("input",SA,null,512),[[o,{state:e.viewState,set:a=>{e.options.origin=a},value:e.options?.origin,form:"anonymize",options:{}}]]),t[113]||(t[113]=n("label",{class:"form-check-label",for:"origin-gist"},"Source gist ID",-1))])],512),[[J,e.detectedType==="gist"]]),T(n("div",null,[n("div",NA,[T(n("input",RA,null,512),[[o,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[114]||(t[114]=n("label",{class:"form-check-label",for:"title"},"PR title",-1))]),n("div",DA,[T(n("input",TA,null,512),[[o,{state:e.viewState,set:a=>{e.options.body=a},value:e.options?.body,form:"anonymize",options:{}}]]),t[115]||(t[115]=n("label",{class:"form-check-label",for:"body"},"PR body",-1))]),n("div",AA,[T(n("input" Line truncated
anonymous_github`)])])])])]),f(" Limitations "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-warning"})]),y("Limitations ")]),n("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),n("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[n("div",{class:"panel-body p-3"},[n("ul",null,[n("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),n("li",null," Files larger than 8 MB are not supported. "),n("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),n("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy & Security "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-shield"})]),y("Privacy & Security ")]),n("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),n("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[n("div",{class:"panel-body p-3"},[n("p",null," Repository data is used to provide the anonymization service. If you purchase a plan, Stripe processes payment and billing information. When a repository is removed or expires, only its configuration is retained. This makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),n("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[n("div",{class:"panel-body p-3"},[n("p",null,[y("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),n("a",{href:"/connections"},"GitHub connections"),y(" page.")]),n("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),n("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),n("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[n("div",{class:"panel-body p-3"},[n("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-Hosting "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#project-icon-storage"})]),y("Self-Hosting ")]),n("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),n("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[n("div",{class:"panel-body p-3"},[n("p",null,[y(" Yes. Line truncated
jane.smith@example.org`},null,10,_A),[[s,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"anonymize",options:{debounce:250}}]]),t[93]||(t[93]=n("small",{class:"form-text text-muted"},"One term per line.",-1)),n("details",OA,[t[88]||(t[88]=n("summary",null,"Regex & replacement syntax",-1)),t[89]||(t[89]=y()),n("small",IA,[t[82]||(t[82]=y("Regex is supported. Replaced by ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK)+"-[N]",1),t[83]||(t[83]=y(", or use ",-1)),t[84]||(t[84]=n("code",null,"term=>replacement",-1)),t[85]||(t[85]=y(" to pick your own (e.g. ",-1)),t[86]||(t[86]=n("code",null,"Anonymous=>ABC",-1)),t[87]||(t[87]=y(").",-1))])]),(l(!0),d(B,null,ie((e.termHints||[]).slice(0,3),a=>(l(),d("div",{class:"term-hint",key:a.line},[n("span",null,[t[90]||(t[90]=y("In ",-1)),n("code",null,c(a.term),1),t[91]||(t[91]=y(", ",-1)),n("code",null,c(a.symbols.join(" ")),1),y(" "+c(a.symbols.length>1?"are regex symbols":"is a regex symbol")+", so it may not match the text as written.",1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm",onClick:u=>e.matchTermAsWritten(a.line)},"Match as written",8,MA)]))),128)),(e.termHints||[]).length>3?(l(),d("div",VA,c(e.termHints.length-3)+" more "+c(e.termHints.length-3===1?"term uses":"terms use")+" regex symbols.",1)):f("v-if",!0),A(n("div",qA,"Terms are in an invalid format.",512),[[J,e.anonymize?.terms?.errors?.format]])]),A(n("div",$A,[n("div",LA,[t[94]||(t[94]=n("span",{class:"paper-field-label",id:"preserve-files-label"},"Unredacted files",-1)),n("span",UA,c(e.preserveSummary()),1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm","aria-describedby":"preserve-files-label",disabled:!e.source?.commit,onClick:t[19]||(t[19]=a=>e.openPreserveFiles())},c((e.options?.preserveFiles||[]).length?"Edit":"Choose files"),9,FA)],512),[[J,e.detectedType==="repo"]]),e.preserve?.open?(l(),d("dialog",{key:0,id:"preserve-files-dialog",class:"preserve-dialog","aria-labelledby":"preserve-files-title",onCancel:t[24]||(t[24]=ce(a=>e.closePreserveFiles(),["prevent"]))},[n("form",{onSubmit:t[23]||(t[23]=ce(a=>e.savePreserveFiles(),["prevent"]))},[t[98]||(t[98]=n("h2",{id:"preserve-files-title"},"Leave files unredacted",-1)),t[99]||(t[99]=n("p",{class:"preserve-intro"},"Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.",-1)),A(n("input",{type:"search",id:"preserve-files-search",class:"form-control",placeholder:"Search files","aria-label":"Search files",autocomplete:"off","onUpdate:modelValue":t[20]||(t[20]=a=>e.preserve.search=a)},null,512),[[ct,e.preserve.search]]),n("div",BA,[e.preserve.loading?(l(),d("p",zA,"Loading files from GitHub\u2026")):e.preserve.error?(l(),d("p",HA,c(e.preserve.error),1)):(l(),d(B,{key:2},[(l(!0),d(B,null,ie(e.preserveRows(),(a,u)=>(l(),d("div",{class:"preserve-item",key:a.kind+":"+a.path,style:$e({paddingLeft:a.depth*18+6+"px"})},[a.kind==="dir"?(l(),d("button",{key:0,type:"button",class:"preserve-caret","aria-expanded":a.open?"true":"false","aria-label":(a.open?"Collapse ":"Expand ")+a.label,onClick:p=>e.togglePreserveDir(a.path)},c(a.open?"\u25BE":"\u25B8"),9,jA)):(l(),d("span",GA)),n("input",{type:"checkbox",id:"pf-"+u,"aria-label":a.kind==="dir"?"Leave folder "+a.path+" unredacted":null,checked:a.state==="all",".indeterminate":a.state==="some",disabled:a.locked,onChange:p=>a.kind==="dir"?e.togglePreserveFolder(a.path):e.togglePreserveFile(a.path)},null,40,WA),n("label",{for:a.kind==="file"?"pf-"+u:null,class:"preserve-name",onClick:p=>a.kind==="dir"&&e.togglePreserveDir(a.path)},c(a.label),9,KA),a.note?(l(),d("span",YA,c(a.note),1)):f("v-if",!0)],4))),128)),!e.preserve.loading&&!e.preserveRows().length?(l(),d("p",XA,c(e.preserve.search?"No file matches your search.":"No text files in this commit."),1)):f("v-if",!0)],64))]),e.preserve.truncated?(l(),d("p",JA,"This repository has too many files to list them all. Use a pattern for files you cannot find.")):f("v-if",!0),n("details",{class:"preserve-patterns",open:e.preserve.patternsOpen},[t[95]||(t[95]=n("summary",null,"Patterns",-1)),A(n("textarea",{class:"form-control",rows:"3","aria-label":"Patterns",placeholder:`*.bib
paper/appendix/**`,"onUpdate:modelValue":t[21]||(t[21]=a=>e.preserve.patterns=a)},null,512),[[ct,e.preserve.patterns]]),t[96]||(t[96]=n("small",{class:"form-text text-muted"},[y("One per line, as in .gitignore: "),n("code",null,"*.bib"),y(" matches in any folder; "),n("code",null,"/refs.bib"),y(" and "),n("code",null,"paper/refs.bib"),y(" match from the repository root; a folder name such as "),n("code",null,"drafts/"),y(" covers everything inside it; "),n("code",null,"!private.bib"),y(" excludes again.")],-1))],8,QA),n("div",ZA,[n("span",xA,c(e.preserve.loading||e.preserve.error?"":e.preserveCount()+(e.preserveCount()===1?" file":" files")+" will be shown as written"),1),n("span",eT,[n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[22]||(t[22]=a=>e.closePreserveFiles())},"Cancel"),t[97]||(t[97]=n("button",{type:"submit",class:"btn btn-ink"},"Save",-1))])])],32)],32)):f("v-if",!0)],512),[[J,e.detectedType]]),A(n("section",tT,[t[120]||(t[120]=n("h2",{class:"paper-section-eyebrow"},"Display",-1)),A(n("div",null,[n("div",nT,[A(n("input",oT,null,512),[[s,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"anonymize",options:{}}]]),t[101]||(t[101]=n("label",{class:"form-check-label",for:"link"},"Keep links",-1))]),n("div",sT,[A(n("input",iT,null,512),[[s,{state:e.viewState,set:a=>{e.options.image=a},value:e.options?.image,form:"anonymize",options:{}}]]),t[102]||(t[102]=n("label",{class:"form-check-label",for:"image"},"Display images",-1))]),n("div",rT,[A(n("input",aT,null,512),[[s,{state:e.viewState,set:a=>{e.options.pdf=a},value:e.options?.pdf,form:"anonymize",options:{}}]]),t[103]||(t[103]=n("label",{class:"form-check-label",for:"pdf"},"Display PDFs",-1))]),n("div",lT,[A(n("input",dT,null,512),[[s,{state:e.viewState,set:a=>{e.options.notebook=a},value:e.options?.notebook,form:"anonymize",options:{}}]]),t[104]||(t[104]=n("label",{class:"form-check-label",for:"notebook"},"Display Notebooks",-1))]),n("div",uT,[A(n("input",{class:"form-check-input",type:"checkbox",id:"page",name:"page",disabled:!e.details?.hasPage||e.details?.pageSource&&e.details?.pageSource?.branch!==e.source?.branch},null,8,cT),[[s,{state:e.viewState,set:a=>{e.options.page=a},value:e.options?.page,form:"anonymize",options:{}}]]),t[105]||(t[105]=n("label",{class:"form-check-label",for:"page"},"GitHub Pages",-1)),e.details?.hasPage?f("v-if",!0):(l(),d("small",pT,c(e.fmt?.translate("WARNINGS.page_not_enabled_on_repo")),1)),e.details?.hasPage&&e.details?.pageSource?.branch&&e.source?.branch&&e.details.pageSource.branch!==e.source.branch?(l(),d("small",fT,c(e.fmt?.translate("WARNINGS.page_branch_mismatch",{pageBranch:e.details?.pageSource?.branch,selectedBranch:e.source?.branch})),1)):f("v-if",!0)])],512),[[J,e.detectedType==="repo"]]),e.detectedType==="repo"&&(e.options.image||e.options.pdf)?(l(),d("p",hT,[...t[106]||(t[106]=[y("Images and PDFs are served unchanged. Review their contents and metadata before sharing. ",-1),n("a",{href:"/faq#formats"},"Supported formats",-1)])])):f("v-if",!0),A(n("div",null,[n("div",mT,[A(n("input",gT,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[107]||(t[107]=n("label",{class:"form-check-label",for:"title-gist"},"Gist description",-1))]),n("div",vT,[A(n("input",yT,null,512),[[s,{state:e.viewState,set:a=>{e.options.content=a},value:e.options?.content,form:"anonymize",options:{}}]]),t[108]||(t[108]=n("label",{class:"form-check-label",for:"content-gist"},"File contents",-1))]),n("div",bT,[A(n("input",wT,null,512),[[s,{state:e.viewState,set:a=>{e.options.comments=a},value:e.options?.comments,form:"anonymize",options:{}}]]),t[109]||(t[109]=n("label",{class:"form-check-label",for:"comments-gist"},"Comments",-1))]),n("div",kT,[A(n("input",CT,null,512),[[s,{state:e.viewState,set:a=>{e.options.username=a},value:e.options?.username,form:"anonymize",options:{}}]]),t[110]||(t[110]=n("label",{class:"form-check-label",for:"username-gist"},"Usernames",-1))]),n("div",ET,[A(n("input",ST,null,512),[[s,{state:e.viewState,set:a=>{e.options.date=a},value:e.options?.date,form:"anonymize",options:{}}]]),t[111]||(t[111]=n("label",{class:"form-check-label",for:"date-gist"},"Dates",-1))]),n("div",NT,[A(n("input",RT,null,512),[[s,{state:e.viewState,set:a=>{e.options.origin=a},value:e.options?.origin,form:"anonymize",options:{}}]]),t[112]||(t[112]=n("label",{class:"form-check-label",for:"origin-gist"},"Source gist ID",-1))])],512),[[J,e.detectedType==="gist"]]),A(n("div",null,[n("div",DT,[A(n("input",AT,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[113]||(t[113]=n("label",{class:"form-check-label",for:"title"},"PR title",-1))]),n("div",TT,[A(n("input",PT,null,512),[[s,{state:e.viewState,set:a=>{e.options.body=a},value:e.options?.body,form:"anonymize",options:{}}]]),t[114]||(t[114]=n("label",{class:"form-check-label",for:"body"},"PR body",-1))]),n("div",_T,[A(n("input" Line truncated
anonymous_github`)])])])])]),f(" Limitations "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-warning"})]),y("Limitations ")]),n("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),n("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[n("div",{class:"panel-body p-3"},[n("ul",null,[n("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),n("li",null," Files larger than 8 MB are not supported. "),n("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),n("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy & Security "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-shield"})]),y("Privacy & Security ")]),n("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),n("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[n("div",{class:"panel-body p-3"},[n("p",null," Repository data is used to provide the anonymization service. If you purchase a plan, Stripe processes payment and billing information. When a repository is removed or expires, only its configuration is retained. This makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),n("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[n("div",{class:"panel-body p-3"},[n("p",null,[y("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),n("a",{href:"/connections"},"GitHub connections"),y(" page.")]),n("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),n("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),n("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[n("div",{class:"panel-body p-3"},[n("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-Hosting "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#project-icon-storage"})]),y("Self-Hosting ")]),n("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),n("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[n("div",{class:"panel-body p-3"},[n("p",null,[y(" Yes. Line truncated
anonymized repository often think it is about the paper, so the
label names the service and the tooltip says who to contact. `),t[26]||(t[26]=Ce('<li class="nav-item d-none d-lg-block"><a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug </a></li><li class="nav-item nav-support-item d-none d-lg-block"><a class="nav-link nav-support-inline" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs"><svg class="ui-icon nav-heart" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>Support </a></li>',2)),e.isDarkMode?f("v-if",!0):(l(),d("li",V6,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to dark mode","aria-label":"Switch to dark mode",onClick:t[1]||(t[1]=s=>{e.darkMode(!0)})},[...t[19]||(t[19]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",q6,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to light mode","aria-label":"Switch to light mode",onClick:t[2]||(t[2]=s=>{e.darkMode(!1)})},[...t[20]||(t[20]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-sun"})],-1)])])])):f("v-if",!0),!e.user&&!e.isReviewerRoute?(l(),d("li",$6,[...t[21]||(t[21]=[n("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[n("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])):f("v-if",!0),e.user?.username&&e.headerCanUpgrade&&!e.headerBillingPending?(l(),d("li",L6,[n("a",{class:"nav-link nav-upgrade",href:"/profile?upgrade=1",onClick:t[3]||(t[3]=ce(s=>e.openHeaderUpgrade(),["prevent"]))},"Upgrade")])):f("v-if",!0),e.user?(l(),d("li",U6,[n("a",F6,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,B6)):f("v-if",!0),n("span",z6,p(e.user?.username),1)]),n("div",H6,[n("h6",j6,"Signed in as @"+p(e.user?.username),1),t[23]||(t[23]=Ce('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item um-extra" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a>',6)),e.headerShowSupport?(l(),d("a",G6,[...t[22]||(t[22]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-heart"})],-1),n("span",null,"Support the project",-1)])])):f("v-if",!0),t[24]||(t[24]=n("div",{class:"dropdown-divider"},null,-1)),t[25]||(t[25]=n("a",{class:"dropdown-item",href:"/api/user/logout",target:"_self"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-logout"})]),n("span",null,"Sign out")],-1))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,W6)):f("v-if",!0),e.emailPromptOpen?(l(),d("dialog",{key:1,id:"email-prompt",class:"email-prompt","aria-labelledby":"email-prompt-title","aria-describedby":"email-prompt-reason",onCancel:t[8]||(t[8]=ce(s=>e.respondToEmailPrompt("later"),["prevent"]))},[n("form",{onSubmit:t[7]||(t[7]=ce(s=>e.respondToEmailPrompt("save"),["prevent"]))},[t[29]||(t[29]=n("h2",{id:"email-prompt-title"},"Stay informed about access problems",-1)),t[30]||(t[30]=n("p",{id:"email-prompt-reason"},"Get email alerts when your GitHub token or repository access fails.",-1)),t[31]||(t[31]=n("label",{for:"email-prompt-address"},"Email address",-1)),T(n("input",{id:"email-prompt-address",class:"form-control",type:"email",autocomplete:"email",maxlength:"254",required:"","onUpdate:modelValue":t[4]||(t[4]=s=>e.emailPromptAddress=s),disabled:e.emailPromptBusy},null,8,K6),[[dt,e.emailPromptAddress]]),e.emailPromptError?(l(),d("p",Y6,p(e.emailPromptError),1)):f("v-if",!0),n("div",X6,[n("button",{type:"button",class:"btn btn-outline-ink",disabled:e.emailPromptBusy,onClick:t[5]||(t[5]=s=>e.respondToEma Line truncated
python experiments/run.py`),n("p",null,[y("Contact: "),n("span",{class:"redact",role:"img","aria-label":"Redacted term 3"},[n("i",null,"3")])])])])])])],-1))]),f(" Live stats "),n("section",iq,[t[9]||(t[9]=n("div",{class:"paper-stats-meta"},[n("div",{class:"paper-stats-meta-left"},[n("span",{class:"paper-stats-dot","aria-hidden":"true"}),y(" Live \xB7 last 60 days \xB7 updated daily")]),n("div",{class:"paper-stats-meta-right"},"Used by authors at ICSE \xB7 FSE \xB7 OOPSLA")],-1)),n("div",rq,[(l(!0),d(W,null,ae(e.cards,(s,i)=>(l(),d("div",{class:"paper-stat-card",key:s?.key},[n("div",{class:_(["paper-stat-value",{"is-pending":!e.stat}])},p(e.stat?e.fmt?.bigNum(s?.total):"\u2014"),3),n("div",aq,p(s?.label),1),e.history[s?.key].bars?.length>1?(l(),d("div",lq," +"+p(e.fmt?.number(e.history[s?.key].deltaToday))+" today ",1)):f("v-if",!0)]))),128))])]),f(" Before / after "),t[25]||(t[25]=n("section",{class:"paper-section-head paper-wrap"},[n("div",null,[n("div",{class:"paper-eyebrow"},"Try it"),n("h2",null,[y("The same repository, "),n("em",null,"minus you.")])]),n("p",null," Names, affiliations, emails, and any term you list are replaced across the README, source files, notebooks, and PDF links. Reviewers browse the real thing, not a zip. ")],-1)),n("div",dq,[n("div",uq,[t[10]||(t[10]=n("label",{for:"demo-terms"},[y("Terms to redact "),n("span",null,"comma-separated, try adding your own")],-1)),T(n("input",{id:"demo-terms",type:"text","onUpdate:modelValue":t[0]||(t[0]=s=>e.demoTerms=s),spellcheck:"false",autocomplete:"off"},null,512),[[dt,e.demoTerms]])]),n("div",cq,[n("div",pq,[t[12]||(t[12]=n("div",{class:"pane-header"},[n("span",{class:"pane-label"},[y("Your repository "),n("span",{class:"pane-edit-hint"},"\xB7 editable")]),n("span",{class:"pane-url"},"github.com/jane-smith/DeepLearnUtils")],-1)),n("div",fq,[n("div",hq,[(l(!0),d(W,null,ae(e.demoParts(),(s,i)=>(l(),d(W,{key:i},[s.redacted?(l(),d("mark",mq,p(s.original),1)):(l(),d(W,{key:1},[y(p(s.text),1)],64))],64))),128)),t[11]||(t[11]=y("\u200B",-1))]),T(n("textarea",{class:"pane-body demo-input","onUpdate:modelValue":t[1]||(t[1]=s=>e.demoText=s),spellcheck:"false","aria-label":"Sample README to anonymize"},null,512),[[dt,e.demoText]])])]),n("div",gq,[n("div",vq,[t[13]||(t[13]=n("span",{class:"pane-label"},"What reviewers see",-1)),n("span",yq,[n("span",bq,p(e.demoCount())+" redacted",1)])]),n("div",wq,[(l(!0),d(W,null,ae(e.demoParts(),(s,i)=>(l(),d(W,{key:i},[s.redacted?(l(),d("span",{key:0,class:"redact-chip",title:"Replaces \u201C"+s.original+"\u201D"},p(s.text),9,kq)):(l(),d(W,{key:1},[y(p(s.text),1)],64))],64))),128))])])])]),f(" What you get: one screenshot, three tabs "),t[26]||(t[26]=n("section",{class:"paper-section-head paper-wrap",id:"about"},[n("div",null,[n("div",{class:"paper-eyebrow"},"What you get"),n("h2",null,"Anonymize, review, manage.")]),n("p",null," Terms are redacted in text files. Enabled images and PDFs are served unchanged, so review them for identifying content before sharing. Monitor your mirror, update it, or remove it when the review is over. ")],-1)),n("section",Cq,[n("div",Eq,[(l(!0),d(W,null,ae(e.features,(s,i)=>(l(),d("div",{class:_(["paper-feature",{"is-on":e.feature===s?.key}]),key:s?.key},[n("button",{type:"button",class:"paper-feature-tab",role:"tab",onClick:o=>e.selectFeature(s.key),onKeydown:o=>e.featureKeydown(o,i),id:"feature-tab-"+s?.key,"aria-controls":"feature-panel-"+s?.key,"aria-selected":e.feature===s?.key,tabindex:e.feature===s?.key?0:-1},[n("span",Nq,p(s?.num)+" \xB7 "+p(s?.eyebrow),1),n("span",Rq,p(s?.title)+" "+p(s?.accent),1)],40,Sq),n("div",Dq,[n("span",Tq,p(s?.title)+" "+p(s?.accent),1),n("p",null,p(s?.text),1),n("a",{class:"paper-link-arrow",href:e.safeUrl(e.featureHref(s)),target:e.featureTarget(s)},p(s?.cta)+" \u2192",9,Aq)])],2))),128))]),n("div",_q,[(l(!0),d(W,null,ae(e.features,(s,i)=>T((l(),d("div",{class:"paper-feature-panel",role:"tabpanel",key:s?.key,id:"feature-panel-"+s?.key,"aria-labelledby":"feature-tab-"+s?.key},[n("div",Pq,[n("div",Iq,[t[14]||(t[14]=n("span",{class:"dots"},[n("i"),n("i"),n("i")],-1)),n("span",Mq,p(s?.url),1)]),n("div",{class:_(["feat-mock","is-"+s?.key]),role:"img","aria-label":s?.alt},[s?.key==="anonymize"?(l(),d("div",qq,[...t[15]||(t[15]=[Ce('<div class="fm-label">GitHub repository</div><div class="fm-input">github.com/jane-smith/DeepLearnUtils</div><div class="fm-label">Terms to redact</div><div class="fm-chips"><span>Jane Smith</span><span>MIT CSAIL</span><span>jane.smith@mit.edu</span><span class="fm-chip-add">+ Add term</span></div><div class="fm-row"><div><div class="fm-label">Reviewers can see</div><div class="fm-toggles"><span class="is-on">Images</span><span class="is-on">PDFs</span><span>Links</span><span class="is-on">Notebooks</span></div></div><div><div class="fm-label">Expires</div><div class="fm-input fm-input-sm">Mar 31, 2027</div></div></div><div class="fm-actions"><span class="fm-share">anonymous.4open.science/r/paper-7f2a</span><span class="fm-btn"> Line truncated
label names the service and the tooltip says who to contact. `),t[26]||(t[26]=ke('<li class="nav-item d-none d-lg-block"><a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug </a></li><li class="nav-item nav-support-item d-none d-lg-block"><a class="nav-link nav-support-inline" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs"><svg class="ui-icon nav-heart" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>Support </a></li>',2)),e.isDarkMode?f("v-if",!0):(l(),d("li",jV,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to dark mode","aria-label":"Switch to dark mode",onClick:t[1]||(t[1]=o=>{e.darkMode(!0)})},[...t[19]||(t[19]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",GV,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to light mode","aria-label":"Switch to light mode",onClick:t[2]||(t[2]=o=>{e.darkMode(!1)})},[...t[20]||(t[20]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-sun"})],-1)])])])):f("v-if",!0),!e.user&&!e.isReviewerRoute?(l(),d("li",WV,[...t[21]||(t[21]=[n("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[n("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])):f("v-if",!0),e.user?.username&&e.headerCanUpgrade&&!e.headerBillingPending?(l(),d("li",KV,[n("a",{class:"nav-link nav-upgrade",href:"/profile?upgrade=1",onClick:t[3]||(t[3]=ce(o=>e.openHeaderUpgrade(),["prevent"]))},"Upgrade")])):f("v-if",!0),e.user?(l(),d("li",YV,[n("a",XV,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,JV)):f("v-if",!0),n("span",QV,c(e.user?.username),1)]),n("div",ZV,[n("h6",xV,"Signed in as @"+c(e.user?.username),1),t[23]||(t[23]=ke('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item um-extra" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a>',6)),e.headerShowSupport?(l(),d("a",eq,[...t[22]||(t[22]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-heart"})],-1),n("span",null,"Support the project",-1)])])):f("v-if",!0),t[24]||(t[24]=n("div",{class:"dropdown-divider"},null,-1)),t[25]||(t[25]=n("a",{class:"dropdown-item",href:"/api/user/logout",target:"_self"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-logout"})]),n("span",null,"Sign out")],-1))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,tq)):f("v-if",!0),e.emailPromptOpen?(l(),d("dialog",{key:1,id:"email-prompt",class:"email-prompt","aria-labelledby":"email-prompt-title","aria-describedby":"email-prompt-reason",onCancel:t[8]||(t[8]=ce(o=>e.respondToEmailPrompt("later"),["prevent"]))},[n("form",{onSubmit:t[7]||(t[7]=ce(o=>e.respondToEmailPrompt("save"),["prevent"]))},[t[29]||(t[29]=n("h2",{id:"email-prompt-title"},"Stay informed about access problems",-1)),t[30]||(t[30]=n("p",{id:"email-prompt-reason"},"Get email alerts when your GitHub token or repository access fails.",-1)),t[31]||(t[31]=n("label",{for:"email-prompt-address"},"Email address",-1)),A(n("input",{id:"email-prompt-address",class:"form-control",type:"email",autocomplete:"email",maxlength:"254",required:"","onUpdate:modelValue":t[4]||(t[4]=o=>e.emailPromptAddress=o),disabled:e.emailPromptBusy},null,8,nq),[[ct,e.emailPromptAddress]]),e.emailPromptError?(l(),d("p",oq,c(e.emailPromptError),1)):f("v-if",!0),n("div",sq,[n("button",{type:"button",class:"btn btn-outline-ink",disabled:e.emailPromptBusy,onClick:t[5]||(t[5]=o=>e.respondToEma Line truncated
python experiments/run.py`),n("p",null,[y("Contact: "),n("span",{class:"redact",role:"img","aria-label":"Redacted term 3"},[n("i",null,"3")])])])])])])],-1))]),f(" Live stats "),n("section",hq,[t[9]||(t[9]=n("div",{class:"paper-stats-meta"},[n("div",{class:"paper-stats-meta-left"},[n("span",{class:"paper-stats-dot","aria-hidden":"true"}),y(" Live \xB7 last 60 days \xB7 updated daily")]),n("div",{class:"paper-stats-meta-right"},"Used by authors at ICSE \xB7 FSE \xB7 OOPSLA")],-1)),n("div",mq,[(l(!0),d(B,null,ie(e.cards,(o,i)=>(l(),d("div",{class:"paper-stat-card",key:o?.key},[n("div",{class:P(["paper-stat-value",{"is-pending":!e.stat}])},c(e.stat?e.fmt?.bigNum(o?.total):"\u2014"),3),n("div",gq,c(o?.label),1),e.history[o?.key].bars?.length>1?(l(),d("div",vq," +"+c(e.fmt?.number(e.history[o?.key].deltaToday))+" today ",1)):f("v-if",!0)]))),128))])]),f(" Before / after "),t[25]||(t[25]=n("section",{class:"paper-section-head paper-wrap"},[n("div",null,[n("div",{class:"paper-eyebrow"},"Try it"),n("h2",null,[y("The same repository, "),n("em",null,"minus you.")])]),n("p",null," Names, affiliations, emails, and any term you list are replaced across the README, source files, notebooks, and PDF links. Reviewers browse the real thing, not a zip. ")],-1)),n("div",yq,[n("div",bq,[t[10]||(t[10]=n("label",{for:"demo-terms"},[y("Terms to redact "),n("span",null,"comma-separated, try adding your own")],-1)),A(n("input",{id:"demo-terms",type:"text","onUpdate:modelValue":t[0]||(t[0]=o=>e.demoTerms=o),spellcheck:"false",autocomplete:"off"},null,512),[[ct,e.demoTerms]])]),n("div",wq,[n("div",kq,[t[12]||(t[12]=n("div",{class:"pane-header"},[n("span",{class:"pane-label"},[y("Your repository "),n("span",{class:"pane-edit-hint"},"\xB7 editable")]),n("span",{class:"pane-url"},"github.com/jane-smith/DeepLearnUtils")],-1)),n("div",Cq,[n("div",Eq,[(l(!0),d(B,null,ie(e.demoParts(),(o,i)=>(l(),d(B,{key:i},[o.redacted?(l(),d("mark",Sq,c(o.original),1)):(l(),d(B,{key:1},[y(c(o.text),1)],64))],64))),128)),t[11]||(t[11]=y("\u200B",-1))]),A(n("textarea",{class:"pane-body demo-input","onUpdate:modelValue":t[1]||(t[1]=o=>e.demoText=o),spellcheck:"false","aria-label":"Sample README to anonymize"},null,512),[[ct,e.demoText]])])]),n("div",Nq,[n("div",Rq,[t[13]||(t[13]=n("span",{class:"pane-label"},"What reviewers see",-1)),n("span",Dq,[n("span",Aq,c(e.demoCount())+" redacted",1)])]),n("div",Tq,[(l(!0),d(B,null,ie(e.demoParts(),(o,i)=>(l(),d(B,{key:i},[o.redacted?(l(),d("span",{key:0,class:"redact-chip",title:"Replaces \u201C"+o.original+"\u201D"},c(o.text),9,Pq)):(l(),d(B,{key:1},[y(c(o.text),1)],64))],64))),128))])])])]),f(" What you get: one screenshot, three tabs "),t[26]||(t[26]=n("section",{class:"paper-section-head paper-wrap",id:"about"},[n("div",null,[n("div",{class:"paper-eyebrow"},"What you get"),n("h2",null,"Anonymize, review, manage.")]),n("p",null," Terms are redacted in text files. Enabled images and PDFs are served unchanged, so review them for identifying content before sharing. Monitor your mirror, update it, or remove it when the review is over. ")],-1)),n("section",_q,[n("div",Oq,[(l(!0),d(B,null,ie(e.features,(o,i)=>(l(),d("div",{class:P(["paper-feature",{"is-on":e.feature===o?.key}]),key:o?.key},[n("button",{type:"button",class:"paper-feature-tab",role:"tab",onClick:s=>e.selectFeature(o.key),onKeydown:s=>e.featureKeydown(s,i),id:"feature-tab-"+o?.key,"aria-controls":"feature-panel-"+o?.key,"aria-selected":e.feature===o?.key,tabindex:e.feature===o?.key?0:-1},[n("span",Mq,c(o?.num)+" \xB7 "+c(o?.eyebrow),1),n("span",Vq,c(o?.title)+" "+c(o?.accent),1)],40,Iq),n("div",qq,[n("span",$q,c(o?.title)+" "+c(o?.accent),1),n("p",null,c(o?.text),1),n("a",{class:"paper-link-arrow",href:e.safeUrl(e.featureHref(o)),target:e.featureTarget(o)},c(o?.cta)+" \u2192",9,Lq)])],2))),128))]),n("div",Uq,[(l(!0),d(B,null,ie(e.features,(o,i)=>A((l(),d("div",{class:"paper-feature-panel",role:"tabpanel",key:o?.key,id:"feature-panel-"+o?.key,"aria-labelledby":"feature-tab-"+o?.key},[n("div",Bq,[n("div",zq,[t[14]||(t[14]=n("span",{class:"dots"},[n("i"),n("i"),n("i")],-1)),n("span",Hq,c(o?.url),1)]),n("div",{class:P(["feat-mock","is-"+o?.key]),role:"img","aria-label":o?.alt},[o?.key==="anonymize"?(l(),d("div",Gq,[...t[15]||(t[15]=[ke('<div class="fm-label">GitHub repository</div><div class="fm-input">github.com/jane-smith/DeepLearnUtils</div><div class="fm-label">Terms to redact</div><div class="fm-chips"><span>Jane Smith</span><span>MIT CSAIL</span><span>jane.smith@mit.edu</span><span class="fm-chip-add">+ Add term</span></div><div class="fm-row"><div><div class="fm-label">Reviewers can see</div><div class="fm-toggles"><span class="is-on">Images</span><span class="is-on">PDFs</span><span>Links</span><span class="is-on">Notebooks</span></div></div><div><div class="fm-label">Expires</div><div class="fm-input fm-input-sm">Mar 31, 2027</div></div></div><div class="fm-actions"><span class="fm-share">anonymous.4open.science/r/paper-7f2a</span><span class="fm-btn"> Line truncated
<span class="ln">2</span><span class="c"># Author: <span class="redact">XXXX-1</span>, <span class="redact">XXXX-2</span></span>
<span class="ln">3</span><span class="k">import</span> torch
<span class="ln">4</span><span class="k">from</span> utils <span class="k">import</span> load_config
@@ -47,15 +47,15 @@ python experiments/run.py`),n("p",null,[y("Contact: "),n("span",{class:"redact",
<span class="ln">7</span> model = build_model(config)
<span class="ln">8</span> <span class="k">for</span> epoch <span class="k">in</span> range(config.epochs):
<span class="ln">9</span> loss = step(model)
<span class="ln">10</span> log(<span class="s">&quot;epoch %d: %.3f&quot;</span>, epoch, loss)</pre></div>`,2)])])):(l(),d("div",Lq,[...t[17]||(t[17]=[Ce('<div class="fm-dash-head"><span>Project</span><span>Status</span><span>Views</span><span>Expires</span></div><div class="fm-dash-row"><span><b>DeepLearnUtils</b><i>paper-7f2a \xB7 ICSE 2027</i></span><span class="st is-ready">Ready</span><span>41</span><span>Mar 31, 2027</span></div><div class="fm-dash-row"><span><b>vision-bench#212</b><i>pull request \xB7 FSE 2027</i></span><span class="st is-ready">Ready</span><span>12</span><span>Never</span></div><div class="fm-dash-row"><span><b>graph-repair</b><i>gr-91ab</i></span><span class="st is-progress">Updating</span><span>5</span><span>Jan 5, 2027</span></div><div class="fm-dash-row is-muted"><span><b>JarSift</b><i>OOPSLA 2025</i></span><span class="st is-expired">Expired</span><span>72</span><span>Sep 28, 2026</span></div>',5)])]))],10,Vq)])],8,Oq)),[[J,e.feature===s?.key]])),128))])]),f(" FAQ teaser "),t[27]||(t[27]=Ce('<section class="paper-qa paper-wrap" aria-labelledby="qa-heading"><div><div class="paper-eyebrow">Before you sign in</div><h2 id="qa-heading">The questions that come up most.</h2><a class="paper-link-arrow" href="/faq">Read the full FAQ \u2192</a></div><div><div class="q"><a href="/faq#permissions">What access does GitHub sign-in require?</a><p> The GitHub App has read-only access to gists and the repositories you choose. Legacy OAuth uses broader permissions. Anonymous GitHub never writes to your repositories. </p></div><div class="q"><a href="/faq#formats">Which file formats are supported?</a><p> Source code with syntax highlighting, Markdown, Jupyter notebooks, PDFs, images, and GitHub Pages sites. Text files are anonymized; enabled images and PDFs are served unchanged. </p></div><div class="q"><a href="/faq#expiration">What happens when an anonymized repository expires?</a><p> You choose: the mirror is removed, or visitors are redirected to the original GitHub repository once the review is over. </p></div></div></section><section class="paper-cta paper-wrap" aria-labelledby="cta-heading"><div><h2 id="cta-heading">Ready for review?</h2><p>Create an anonymous mirror in a couple of minutes. Reviewers just open the link.</p></div><div class="paper-cta-actions"><a class="btn-hero" href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a><a class="paper-link-arrow" href="/faq">Read the FAQ \u2192</a></div></section>',2)),n("footer",Uq,[n("div",Fq,[t[21]||(t[21]=n("div",{class:"paper-footer-brand"},[n("div",{class:"paper-footer-mark"},[y("Anonymous "),n("em",null,"GitHub")]),n("p",{class:"paper-footer-tag"}," A read-only, identity-stripped mirror of your repository, built for double-anonymous peer review. ")],-1)),n("div",Bq,[t[18]||(t[18]=n("div",{class:"paper-footer-head"},"Product",-1)),t[19]||(t[19]=n("a",{href:"/anonymize"},"Anonymize",-1)),e.user?(l(),d("a",zq,"My work")):f("v-if",!0),e.user?(l(),d("a",Hq,"Conferences")):f("v-if",!0),t[20]||(t[20]=n("a",{href:"/faq"},"FAQ",-1))]),t[22]||(t[22]=Ce('<div class="paper-footer-col"><div class="paper-footer-head">Project</div><a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a><a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a><a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Support</a></div><div class="paper-footer-col"><div class="paper-footer-head">Contact</div><a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a><a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a></div>',2))]),t[24]||(t[24]=n("div",{class:"paper-footer-rule"},null,-1)),n("div",jq,[n("span",null,"\xA9 "+p(e.year||2026)+" Anonymous GitHub \xB7 GPL-3.0 licensed",1),t[23]||(t[23]=n("span",{class:"paper-footer-meta"},"Built for reviewers, by researchers.",-1))])])])}var Gq={class:"paper-empty"},Wq={class:"paper-empty-inner"},Kq={key:0,class:"paper-eyebrow"},Yq={key:1,class:"paper-eyebrow"},Xq={key:2,class:"paper-empty-title"},Jq=["textContent"];function _c(e,t){return l(),d("div",Gq,[n("div",Wq,[e.error?f("v-if",!0):(l(),d("div",Kq,"Working")),e.error?(l(),d("div",Yq,"Error")):f("v-if",!0),e.error?f("v-if",!0):(l(),d("h1",Xq,[...t[0]||(t[0]=[y(" Loading",-1),n("span",{class:"dot-anim"},"\u2026",-1)])])),e.error?(l(),d("h1",{key:3,class:"paper-empty-title",textContent:p(e.fmt.translate(e.error))},null,8,Jq)):f("v-if",!0)])])}var Qq={class:"container paper-page paper-settings"},Zq={class:"paper-crumbs"},xq={class:"here"},e$={class:"paper-page-title"},t$={class:"paper-settings-body"},n$={class:"paper-settings-toc"},s$={href:"#conf-billing"},o$={class:"form needs-validation paper-settings-main",name:"conference",novalidate:""},i$={id:"conf-basics",class:"paper-settings-section"},r$={class:"form-group"},a$={class:"invalLine truncated
`).length))+" lines",1)):f("v-if",!0),e.type=="html"&&e.fileSize(e.filePath)!=null?(l(),d("span",yL,p(e.formatSize(e.fileSize(e.filePath))),1)):f("v-if",!0),e.redactionCount()?(l(),d("b",bL,p(e.redactionCount())+" redacted",1)):f("v-if",!0)]),T(n("button",{type:"button",class:"btn btn-sm btn-icon-only btn-ghost copy-path-btn","aria-label":e.pathCopied?"Path copied":"Copy file path",title:e.pathCopied?"Path copied":"Copy file path",onClick:t[2]||(t[2]=a=>e.copyPath())},[(l(),d("svg",kL,[n("use",{href:e.pathCopied?"#ui-icon-check":"#ui-icon-link"},null,8,CL)]))],8,wL),[[J,e.filePath]])]),e.type!="html"&&typeof e.content=="string"?(l(),d("span",EL,[e.languageName?(l(),d(W,{key:0},[y(p(e.languageName),1),t[37]||(t[37]=n("span",{class:"sep"},"\xB7",-1))],64)):f("v-if",!0),y(p(e.fmt?.number(e.content.split(`
`).length))+" lines",1),n("span",SL,[t[38]||(t[38]=n("span",{class:"sep"},"\xB7",-1)),y(p(e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)]),e.redactionCount()?(l(),d(W,{key:1},[t[39]||(t[39]=n("span",{class:"sep"},"\xB7",-1)),n("span",NL,p(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("span",RL,[t[42]||(t[42]=y("Markdown",-1)),e.fileSize(e.filePath)!=null?(l(),d(W,{key:0},[t[40]||(t[40]=n("span",{class:"sep"},"\xB7",-1)),y(p(e.formatSize(e.fileSize(e.filePath))),1)],64)):f("v-if",!0),e.redactionCount()?(l(),d(W,{key:1},[t[41]||(t[41]=n("span",{class:"sep"},"\xB7",-1)),n("span",DL,p(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),n("div",TL,[e.type!="html"&&e.symbols().length>1?(l(),d("div",AL,[n("button",{type:"button",class:_(["btn btn-sm btn-ghost btn-symbols",{"is-on":e.symbolsOpen}]),"aria-expanded":e.symbolsOpen?"true":"false","aria-haspopup":"true",title:"Symbols in this file",onClick:t[3]||(t[3]=a=>e.symbolsOpen=!e.symbolsOpen)},[...t[43]||(t[43]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-list"})],-1),n("span",{class:"btn-find-label"},"Symbols",-1)])],10,_L),e.symbolsOpen?(l(),d("div",OL,[n("div",PL,p(e.symbols().length)+" "+p(e.symbols().length===1?"symbol":"symbols"),1),(l(!0),d(W,null,ae(e.symbols(),a=>(l(),d("button",{type:"button",class:"symbols-item",role:"menuitem",key:a.line+a.name,onClick:u=>e.goToSymbol(a.line)},[n("span",{class:_(["symbols-kind","is-"+a.kind])},p(a.kind==="class"?"C":a.kind==="function"?"\u0192":"v"),3),n("span",ML,p(a.name),1),n("span",VL,p(a.line),1)],8,IL))),128))])):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("button",{key:1,type:"button",class:_(["btn btn-sm btn-ghost btn-icon-only btn-find btn-md-find",{"is-on":e.mdFindOpen}]),"aria-label":"Find on this page",title:"Find on this page ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[4]||(t[4]=a=>e.toggleMdFind())},[...t[44]||(t[44]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],10,qL)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:2,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-find","aria-label":"Find in file",title:"Find in file ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[5]||(t[5]=a=>e.findInFile())},[...t[45]||(t[45]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],8,$L)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:3,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-goto","aria-label":"Go to line",title:"Go to line ("+(e.isMac?"\u2318":"Ctrl+")+"G)",onClick:t[6]||(t[6]=a=>e.gotoLine())},[...t[46]||(t[46]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1)])],8,LL)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:4,type:"button",class:_(["btn btn-sm btn-icon-only btn-ghost btn-toggle",{"is-on":e.wrapLines}]),"aria-pressed":e.wrapLines?"true":"false","aria-label":"Wrap long lines",title:"Wrap long lines (Alt+Z)",onClick:t[7]||(t[7]=a=>e.toggleWrap())},[...t[47]||(t[47]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)])],10,UL)):f("v-if",!0),n("div",FL,[n("a",{target:"_self",class:"tb-seg-btn",title:e.type=="html"?"Open the Markdown source":"Open the raw file",href:e.safeUrl(e.url)},"Raw",8,BL),typeof e.content=="string"?(l(),d("button",{key:0,type:"button",class:"tb-seg-btn tb-seg-icon","aria-label":e.contentCopied?"Copied":"Copy file contents",title:e.contentCopied?"Copied":"Copy file contents",onClick:t[8]||(t[8]=a=>e.copyContent())},[(l(),d("svg",HL,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,jL)]))],8,zL)):f("v-if",!0),n("a",{target:"_self",class:"tb-seg-btn tb-seg-icon","aria-label":"Download file",title:"Download file",href:e.safeUrl(e.url+"&download=true")},[...t[48]||(t[48]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1)])],8,GL)]),typeof e.content=="string"?(l(),d("button",{key:5,type:"button",class:"btn btn-sm btn-icon-only btn-ghost tb-copy-m","aria-label":e.contentCopied?"Copied":"Copy file contents",onClick:t[9]||(t[9]=a=>e.copyContent())},[(l(),d("svg",KL,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,YL)]))],8,WL)):f("v-if",!0),e.type=="html"&&e.readmeToc().length>=3?(l(),d("div",XL,[n("button",{type:"button",class:_(["btn btn-sm btn-icon-only btn-ghost tb-toc-btn",{"is-on":e.tocOpen}]),"aria-label":"On this page",title:"On this page","aria-haspopup":"true","aria-expanded":e.tocOpen?"true":"false",onClick:t[10]||(t[10]=ce(a=>{e.tocOpen=!e.tocOpen,e.moreOpen=!1},["stop"]))},[...t[49]||(t[49]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-outline"})],-1)])],10,JL),e.tocOpen?(l(),d("div",{key:0,class:"tb-menu tb-toc-menu",role:"menu",onClick:t[12]||(t[12]=a=>e.tocOpen=!1)},[t[50]||(t[50]=n("div",{class:"tb-menu-label"},"On this page",-1)),n("button Line truncated
`).length))+" lines",1)],64)):f("v-if",!0),y(" \xB7 "+p(e.type=="html"&&e.fileSize(e.filePath)!=null?e.formatSize(e.fileSize(e.filePath)):e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)])):f("v-if",!0),e.options?.publishedAt||e.options?.sourceCommitDate||e.options?.anonymizedAt||e.options?.lastUpdateDate?(l(),d("div",sU)):f("v-if",!0),e.type=="html"?(l(),d("button",{key:3,type:"button",role:"menuitem",onClick:t[14]||(t[14]=a=>e.toggleMdFind())},[...t[53]||(t[53]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type=="html"?(l(),d("div",oU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:5,type:"button",role:"menuitem",onClick:t[15]||(t[15]=a=>e.findInFile())},[...t[54]||(t[54]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:6,type:"button",role:"menuitem",onClick:t[16]||(t[16]=a=>e.gotoLine())},[...t[55]||(t[55]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1),y("Go to line",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:7,type:"button",role:"menuitemcheckbox","aria-checked":e.wrapLines?"true":"false",onClick:t[17]||(t[17]=a=>e.toggleWrap())},[t[56]||(t[56]=n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)),t[57]||(t[57]=y("Wrap lines",-1)),e.wrapLines?(l(),d("span",rU,"\u2713")):f("v-if",!0)],8,iU)):f("v-if",!0),e.type!="html"?(l(),d("div",aU)):f("v-if",!0),e.type=="html"?(l(),d("a",{key:9,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[58]||(t[58]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,lU)):f("v-if",!0),e.type!="html"?(l(),d("a",{key:10,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[59]||(t[59]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,dU)):f("v-if",!0),n("a",{role:"menuitem",target:"_self",href:e.safeUrl(e.url+"&download=true")},[...t[60]||(t[60]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1),y("Download file",-1)])],8,uU),e.options?.hasWebsite?(l(),d("a",{key:11,role:"menuitem",target:"_self",href:e.safeUrl("/w/"+e.repoId+"/")},[...t[61]||(t[61]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-globe"})],-1),y("Open website",-1)])],8,cU)):f("v-if",!0),e.options?.isAdmin||e.options?.isOwner?(l(),d("a",{key:12,role:"menuitem",href:e.safeUrl("/anonymize/"+e.repoId)},[...t[62]||(t[62]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-edit"})],-1),y("Edit anonymization",-1)])],8,pU)):f("v-if",!0),e.options?.download?(l(),d("a",{key:13,role:"menuitem",target:"_self",href:e.safeUrl("/api/repo/"+e.repoId+"/zip")},[...t[63]||(t[63]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-archive"})],-1),y("Download ZIP",-1)])],8,fU)):f("v-if",!0),n("button",{type:"button",role:"menuitem",onClick:t[18]||(t[18]=a=>e.copyPath())},[...t[64]||(t[64]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-link"})],-1),y("Copy file path",-1)])]),t[65]||(t[65]=n("div",{class:"tb-menu-sep"},null,-1)),t[66]||(t[66]=n("a",{role:"menuitem",href:"/faq"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-help"})]),y("FAQ")],-1)),t[67]||(t[67]=n("a",{role:"menuitem",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",target:"_blank",rel:"noopener"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-bug"})]),y("Report a bug")],-1))])):f("v-if",!0)])]),e.gotoOpen&&e.type!="html"?(l(),d("form",{key:2,class:"goto-pop",role:"dialog","aria-label":"Go to line",onSubmit:t[22]||(t[22]=ce(a=>e.submitGoto(),["prevent"]))},[t[68]||(t[68]=n("label",{class:"goto-label",for:"goto-input"},"Go to line",-1)),T(n("input",{id:"goto-input",class:"goto-input",type:"text",inputmode:"numeric",autocomplete:"off",spellcheck:"false",placeholder:"1\u2013"+e.fmt?.number(typeof e.content=="string"?e.content.split(`
`).length:1),"aria-invalid":e.gotoError?"true":"false","onUpdate:modelValue":t[20]||(t[20]=a=>e.gotoValue=a),onKeydown:t[21]||(t[21]=Tt(ce(a=>e.closeGoto(),["prevent","stop"]),["esc"]))},null,40,hU),[[dt,e.gotoValue]]),n("span",{class:_(["goto-hint",{"is-error":e.gotoError}])},p(e.gotoError||"Line, or line:column"),3)],32)):f("v-if",!0)]),e.type!="html"?T((l(),d("div",mU,null,512)),[[r,{content:e.content,options:e.aceOption}]]):f("v-if",!0),e.type=="html"&&e.mdFindOpen?(l(),d("div",gU,[n("input",{type:"search",class:"md-find-input",placeholder:"Find on this page","aria-label":"Find on this page",value:e.mdFindQuery,onInput:t[23]||(t[23]=a=>{e.mdFindQuery=a.target.value,e.runMdFind()}),onKeydown:[t[24]||(t[24]=Tt(ce(a=>a.shiftKey?e.stepMdFind(-1):e.stepMdFind(1),["prevent"]),["enter"])),t[25]||(t[25]=Tt(ce(a=>e.toggleMdFind(!1),["prevent"]),["esc"]))]},null,40,vU),n("span",yU,[n("button",{type:"button","aria-label":"Previous match",onClick:t[26]||(t[26]=a=>e.stepMdFind(-1)),disabled:!e.mdFindTotal},[...t[69]||(t[69]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-left"})],-1)])],8,bU),n("button",{type:"button","aria-label":"Next match",onClick:t[27]||(t[27]=a=>e.stepMdFind(1)),disabled:!e.mdFindTotal},[...t[70]||(t[70]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-right"})],-1)])],8,wU)]),n("span",kU,p(e.mdFindQuery?e.mdFindTotal?e.mdFindIndex+1+" of "+e.mdFindTotal:"No results":""),1),n("button",{type:"button",class:_(["md-find-opt",{"is-on":e.mdFindCase}]),"aria-pressed":e.mdFindCase?"true":"false",title:"Match case","aria-label":"Match case",onClick:t[28]||(t[28]=a=>{e.mdFindCase=!e.mdFindCase,e.runMdFind()})},"Aa",10,CU),n("button",{type:"button",class:"md-find-close","aria-label":"Close find",onClick:t[29]||(t[29]=a=>e.toggleMdFind(!1))},"\xD7")])):f("v-if",!0),e.type=="html"?(l(),d("div",EU,[n("div",{class:"file-content markdown-body",innerHTML:e.renderReadme(e.sanitize(e.content))},null,8,SU),e.readmeToc().length>=3?(l(),d("aside",NU,[t[71]||(t[71]=n("div",{class:"readme-toc-head"},"On this page",-1)),n("nav",RU,[n("a",{href:"#",class:_(["lvl-2 toc-top",{"is-active":!e.activeHeading}]),onClick:t[30]||(t[30]=ce(a=>e.scrollToHeading(null),["prevent"]))},"Introduction",2),(l(!0),d(W,null,ae(e.readmeToc(),(a,u)=>(l(),d("a",{key:a.id,href:"#"+a.id,class:_(["lvl-"+a.level,{"is-active":e.activeHeading===a.id}]),onClick:ce(c=>e.scrollToHeading(a.id),["prevent"])},p(a.text),11,DU))),128))])])):f("v-if",!0)])):f("v-if",!0)],2)):f("v-if",!0),e.type=="html-doc"&&!e.showSource&&e.content!=null?(l(),sn(s,{key:2,content:e.content,"base-url":e.fileBaseUrl,"allow-scripts":e.allowScripts},null,8,["content","base-url","allow-scripts"])):f("v-if",!0),e.type=="image"?(l(),d("img",{key:3,class:"image-content",src:e.safeUrl(e.url)},null,8,TU)):f("v-if",!0),e.type=="media"?(l(),d("iframe",{key:4,class:"h-100 overflow-auto w-100 b-0",src:e.safeUrl(e.url)},null,8,AU)):f("v-if",!0),e.type=="pdf"?(l(),sn(i,{key:5,src:e.safeUrl(e.url)},null,8,["src"])):f("v-if",!0),e.type=="audio"?(l(),d("div",_U,[n("audio",OU,[n("source",{src:e.safeUrl(e.url)},null,8,PU)])])):f("v-if",!0),e.type=="IPython"?(l(),d("div",IU,[Ue(o,{file:e.url,content:e.content},null,8,["file","content"])])):f("v-if",!0),e.type=="rate_limited"?(l(),d("div",MU,[n("div",VU,[t[74]||(t[74]=Ce('<div class="paper-ratelimit-head"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg><div><div class="paper-error-eyebrow">Temporarily paused</div><div class="paper-error-title">GitHub API rate limit reached</div></div></div>',1)),n("p",qU,[t[72]||(t[72]=y("This repository will be available in ",-1)),n("strong",null,p(e.rateLimitCountdown),1),t[73]||(t[73]=y(". The page will reload automatically.",-1))])])])):f("v-if",!0),e.type=="error"?(l(),d("div",$U,[n("section",LU,[n("h1",UU,p(e.fmt.translate(e.isSourceAccessError()?"VIEWER_ERRORS.access_title":"VIEWER_ERRORS.title")),1),n("p",null,p(e.fmt.translate("ERRORS."+e.content)),1),e.isSourceAccessError()?(l(),d(W,{key:0},[n("p",null,p(e.fmt.translate("VIEWER_ERRORS.reader_help")),1),n("p",null,p(e.fmt.translate("VIEWER_ERRORS.owner_help")),1)],64)):f("v-if",!0),n("div",FU,[e.isSourceAccessError()?f("v-if",!0):(l(),d("a",BU,"Back to home")),e.isSourceAccessError()?f("v-if",!0):(l(),d("a",zU,"Read the FAQ")),n("button",{type:"button",class:"btn btn-primary",onClick:t[31]||(t[31]=a=>e.retryContent())},p(e.fmt.translate("VIEWER_ERRORS.retry")),1),e.isSourceAccessError()?(l(),d("a",HU,p(e.fmt.translate("VIEWER_ERRORS.dashboard")),1)):f("v-if",!0)])])])):f("v-if",!0),e.type=="loading"&&!e.error?(l(),d("section",jU,[n("div",GU,[n("strong",null,p(e.fmt.translate(e.fileReceivedBytes?"VIEWER_LOADING.receiving":"VIEWER_LOADING.loading")),1),n("span",WU,p(e.filePath),1),e.fileLoadingSlow?(l(),d("p",KU,p(e.fmt.translate("VIEWER_LOADING.slow")),1)):f("v-if",!0)]),n("div",YU,[(l(),d(W,null,ae(10,a=>n("span",{key:a})),64))])])):f("v-if",!0),e.type== Line truncated
<span class="ln">10</span> log(<span class="s">&quot;epoch %d: %.3f&quot;</span>, epoch, loss)</pre></div>`,2)])])):(l(),d("div",Kq,[...t[17]||(t[17]=[ke('<div class="fm-dash-head"><span>Project</span><span>Status</span><span>Views</span><span>Expires</span></div><div class="fm-dash-row"><span><b>DeepLearnUtils</b><i>paper-7f2a \xB7 ICSE 2027</i></span><span class="st is-ready">Ready</span><span>41</span><span>Mar 31, 2027</span></div><div class="fm-dash-row"><span><b>vision-bench#212</b><i>pull request \xB7 FSE 2027</i></span><span class="st is-ready">Ready</span><span>12</span><span>Never</span></div><div class="fm-dash-row"><span><b>graph-repair</b><i>gr-91ab</i></span><span class="st is-progress">Updating</span><span>5</span><span>Jan 5, 2027</span></div><div class="fm-dash-row is-muted"><span><b>JarSift</b><i>OOPSLA 2025</i></span><span class="st is-expired">Expired</span><span>72</span><span>Sep 28, 2026</span></div>',5)])]))],10,jq)])],8,Fq)),[[J,e.feature===o?.key]])),128))])]),f(" FAQ teaser "),t[27]||(t[27]=ke('<section class="paper-qa paper-wrap" aria-labelledby="qa-heading"><div><div class="paper-eyebrow">Before you sign in</div><h2 id="qa-heading">The questions that come up most.</h2><a class="paper-link-arrow" href="/faq">Read the full FAQ \u2192</a></div><div><div class="q"><a href="/faq#permissions">What access does GitHub sign-in require?</a><p> The GitHub App has read-only access to gists and the repositories you choose. Legacy OAuth uses broader permissions. Anonymous GitHub never writes to your repositories. </p></div><div class="q"><a href="/faq#formats">Which file formats are supported?</a><p> Source code with syntax highlighting, Markdown, Jupyter notebooks, PDFs, images, and GitHub Pages sites. Text files are anonymized; enabled images and PDFs are served unchanged. </p></div><div class="q"><a href="/faq#expiration">What happens when an anonymized repository expires?</a><p> You choose: the mirror is removed, or visitors are redirected to the original GitHub repository once the review is over. </p></div></div></section><section class="paper-cta paper-wrap" aria-labelledby="cta-heading"><div><h2 id="cta-heading">Ready for review?</h2><p>Create an anonymous mirror in a couple of minutes. Reviewers just open the link.</p></div><div class="paper-cta-actions"><a class="btn-hero" href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a><a class="paper-link-arrow" href="/faq">Read the FAQ \u2192</a></div></section>',2)),n("footer",Yq,[n("div",Xq,[t[21]||(t[21]=n("div",{class:"paper-footer-brand"},[n("div",{class:"paper-footer-mark"},[y("Anonymous "),n("em",null,"GitHub")]),n("p",{class:"paper-footer-tag"}," A read-only, identity-stripped mirror of your repository, built for double-anonymous peer review. ")],-1)),n("div",Jq,[t[18]||(t[18]=n("div",{class:"paper-footer-head"},"Product",-1)),t[19]||(t[19]=n("a",{href:"/anonymize"},"Anonymize",-1)),e.user?(l(),d("a",Qq,"My work")):f("v-if",!0),e.user?(l(),d("a",Zq,"Conferences")):f("v-if",!0),t[20]||(t[20]=n("a",{href:"/faq"},"FAQ",-1))]),t[22]||(t[22]=ke('<div class="paper-footer-col"><div class="paper-footer-head">Project</div><a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a><a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a><a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Support</a></div><div class="paper-footer-col"><div class="paper-footer-head">Contact</div><a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a><a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a></div>',2))]),t[24]||(t[24]=n("div",{class:"paper-footer-rule"},null,-1)),n("div",xq,[n("span",null,"\xA9 "+c(e.year||2026)+" Anonymous GitHub \xB7 GPL-3.0 licensed",1),t[23]||(t[23]=n("span",{class:"paper-footer-meta"},"Built for reviewers, by researchers.",-1))])])])}var e$={class:"paper-empty"},t$={class:"paper-empty-inner"},n$={key:0,class:"paper-eyebrow"},o$={key:1,class:"paper-eyebrow"},s$={key:2,class:"paper-empty-title"},i$=["textContent"];function Oc(e,t){return l(),d("div",e$,[n("div",t$,[e.error?f("v-if",!0):(l(),d("div",n$,"Working")),e.error?(l(),d("div",o$,"Error")):f("v-if",!0),e.error?f("v-if",!0):(l(),d("h1",s$,[...t[0]||(t[0]=[y(" Loading",-1),n("span",{class:"dot-anim"},"\u2026",-1)])])),e.error?(l(),d("h1",{key:3,class:"paper-empty-title",textContent:c(e.fmt.translate(e.error))},null,8,i$)):f("v-if",!0)])])}var r$={class:"container paper-page paper-settings"},a$={class:"paper-crumbs"},l$={class:"here"},d$={class:"paper-page-title"},u$={class:"paper-settings-body"},c$={class:"paper-settings-toc"},p$={href:"#conf-billing"},f$={class:"form needs-validation paper-settings-main",name:"conference",novalidate:""},h$={id:"conf-basics",class:"paper-settings-section"},m$={class:"form-group"},g$={class:"invalLine truncated
`).length))+" lines",1)):f("v-if",!0),e.type=="html"&&e.fileSize(e.filePath)!=null?(l(),d("span",qL,c(e.formatSize(e.fileSize(e.filePath))),1)):f("v-if",!0),e.redactionCount()?(l(),d("b",$L,c(e.redactionCount())+" redacted",1)):f("v-if",!0)]),A(n("button",{type:"button",class:"btn btn-sm btn-icon-only btn-ghost copy-path-btn","aria-label":e.pathCopied?"Path copied":"Copy file path",title:e.pathCopied?"Path copied":"Copy file path",onClick:t[2]||(t[2]=a=>e.copyPath())},[(l(),d("svg",UL,[n("use",{href:e.pathCopied?"#ui-icon-check":"#ui-icon-link"},null,8,FL)]))],8,LL),[[J,e.filePath]])]),e.type!="html"&&typeof e.content=="string"?(l(),d("span",BL,[e.languageName?(l(),d(B,{key:0},[y(c(e.languageName),1),t[37]||(t[37]=n("span",{class:"sep"},"\xB7",-1))],64)):f("v-if",!0),y(c(e.fmt?.number(e.content.split(`
`).length))+" lines",1),n("span",zL,[t[38]||(t[38]=n("span",{class:"sep"},"\xB7",-1)),y(c(e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)]),e.redactionCount()?(l(),d(B,{key:1},[t[39]||(t[39]=n("span",{class:"sep"},"\xB7",-1)),n("span",HL,c(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("span",jL,[t[42]||(t[42]=y("Markdown",-1)),e.fileSize(e.filePath)!=null?(l(),d(B,{key:0},[t[40]||(t[40]=n("span",{class:"sep"},"\xB7",-1)),y(c(e.formatSize(e.fileSize(e.filePath))),1)],64)):f("v-if",!0),e.redactionCount()?(l(),d(B,{key:1},[t[41]||(t[41]=n("span",{class:"sep"},"\xB7",-1)),n("span",GL,c(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),n("div",WL,[e.type!="html"&&e.symbols().length>1?(l(),d("div",KL,[n("button",{type:"button",class:P(["btn btn-sm btn-ghost btn-symbols",{"is-on":e.symbolsOpen}]),"aria-expanded":e.symbolsOpen?"true":"false","aria-haspopup":"true",title:"Symbols in this file",onClick:t[3]||(t[3]=a=>e.symbolsOpen=!e.symbolsOpen)},[...t[43]||(t[43]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-list"})],-1),n("span",{class:"btn-find-label"},"Symbols",-1)])],10,YL),e.symbolsOpen?(l(),d("div",XL,[n("div",JL,c(e.symbols().length)+" "+c(e.symbols().length===1?"symbol":"symbols"),1),(l(!0),d(B,null,ie(e.symbols(),a=>(l(),d("button",{type:"button",class:"symbols-item",role:"menuitem",key:a.line+a.name,onClick:u=>e.goToSymbol(a.line)},[n("span",{class:P(["symbols-kind","is-"+a.kind])},c(a.kind==="class"?"C":a.kind==="function"?"\u0192":"v"),3),n("span",ZL,c(a.name),1),n("span",xL,c(a.line),1)],8,QL))),128))])):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("button",{key:1,type:"button",class:P(["btn btn-sm btn-ghost btn-icon-only btn-find btn-md-find",{"is-on":e.mdFindOpen}]),"aria-label":"Find on this page",title:"Find on this page ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[4]||(t[4]=a=>e.toggleMdFind())},[...t[44]||(t[44]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],10,eU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:2,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-find","aria-label":"Find in file",title:"Find in file ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[5]||(t[5]=a=>e.findInFile())},[...t[45]||(t[45]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],8,tU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:3,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-goto","aria-label":"Go to line",title:"Go to line ("+(e.isMac?"\u2318":"Ctrl+")+"G)",onClick:t[6]||(t[6]=a=>e.gotoLine())},[...t[46]||(t[46]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1)])],8,nU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:4,type:"button",class:P(["btn btn-sm btn-icon-only btn-ghost btn-toggle",{"is-on":e.wrapLines}]),"aria-pressed":e.wrapLines?"true":"false","aria-label":"Wrap long lines",title:"Wrap long lines (Alt+Z)",onClick:t[7]||(t[7]=a=>e.toggleWrap())},[...t[47]||(t[47]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)])],10,oU)):f("v-if",!0),n("div",sU,[n("a",{target:"_self",class:"tb-seg-btn",title:e.type=="html"?"Open the Markdown source":"Open the raw file",href:e.safeUrl(e.url)},"Raw",8,iU),typeof e.content=="string"?(l(),d("button",{key:0,type:"button",class:"tb-seg-btn tb-seg-icon","aria-label":e.contentCopied?"Copied":"Copy file contents",title:e.contentCopied?"Copied":"Copy file contents",onClick:t[8]||(t[8]=a=>e.copyContent())},[(l(),d("svg",aU,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,lU)]))],8,rU)):f("v-if",!0),n("a",{target:"_self",class:"tb-seg-btn tb-seg-icon","aria-label":"Download file",title:"Download file",href:e.safeUrl(e.url+"&download=true")},[...t[48]||(t[48]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1)])],8,dU)]),typeof e.content=="string"?(l(),d("button",{key:5,type:"button",class:"btn btn-sm btn-icon-only btn-ghost tb-copy-m","aria-label":e.contentCopied?"Copied":"Copy file contents",onClick:t[9]||(t[9]=a=>e.copyContent())},[(l(),d("svg",cU,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,pU)]))],8,uU)):f("v-if",!0),e.type=="html"&&e.readmeToc().length>=3?(l(),d("div",fU,[n("button",{type:"button",class:P(["btn btn-sm btn-icon-only btn-ghost tb-toc-btn",{"is-on":e.tocOpen}]),"aria-label":"On this page",title:"On this page","aria-haspopup":"true","aria-expanded":e.tocOpen?"true":"false",onClick:t[10]||(t[10]=ce(a=>{e.tocOpen=!e.tocOpen,e.moreOpen=!1},["stop"]))},[...t[49]||(t[49]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-outline"})],-1)])],10,hU),e.tocOpen?(l(),d("div",{key:0,class:"tb-menu tb-toc-menu",role:"menu",onClick:t[12]||(t[12]=a=>e.tocOpen=!1)},[t[50]||(t[50]=n("div",{class:"tb-menu-label"},"On this page",-1)),n("button Line truncated
`).length))+" lines",1)],64)):f("v-if",!0),y(" \xB7 "+c(e.type=="html"&&e.fileSize(e.filePath)!=null?e.formatSize(e.fileSize(e.filePath)):e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)])):f("v-if",!0),e.options?.publishedAt||e.options?.sourceCommitDate||e.options?.anonymizedAt||e.options?.lastUpdateDate?(l(),d("div",kU)):f("v-if",!0),e.type=="html"?(l(),d("button",{key:3,type:"button",role:"menuitem",onClick:t[14]||(t[14]=a=>e.toggleMdFind())},[...t[53]||(t[53]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type=="html"?(l(),d("div",CU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:5,type:"button",role:"menuitem",onClick:t[15]||(t[15]=a=>e.findInFile())},[...t[54]||(t[54]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:6,type:"button",role:"menuitem",onClick:t[16]||(t[16]=a=>e.gotoLine())},[...t[55]||(t[55]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1),y("Go to line",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:7,type:"button",role:"menuitemcheckbox","aria-checked":e.wrapLines?"true":"false",onClick:t[17]||(t[17]=a=>e.toggleWrap())},[t[56]||(t[56]=n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)),t[57]||(t[57]=y("Wrap lines",-1)),e.wrapLines?(l(),d("span",SU,"\u2713")):f("v-if",!0)],8,EU)):f("v-if",!0),e.type!="html"?(l(),d("div",NU)):f("v-if",!0),e.type=="html"?(l(),d("a",{key:9,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[58]||(t[58]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,RU)):f("v-if",!0),e.type!="html"?(l(),d("a",{key:10,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[59]||(t[59]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,DU)):f("v-if",!0),n("a",{role:"menuitem",target:"_self",href:e.safeUrl(e.url+"&download=true")},[...t[60]||(t[60]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1),y("Download file",-1)])],8,AU),e.options?.hasWebsite?(l(),d("a",{key:11,role:"menuitem",target:"_self",href:e.safeUrl("/w/"+e.repoId+"/")},[...t[61]||(t[61]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-globe"})],-1),y("Open website",-1)])],8,TU)):f("v-if",!0),e.options?.isAdmin||e.options?.isOwner?(l(),d("a",{key:12,role:"menuitem",href:e.safeUrl("/anonymize/"+e.repoId)},[...t[62]||(t[62]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-edit"})],-1),y("Edit anonymization",-1)])],8,PU)):f("v-if",!0),e.options?.download?(l(),d("a",{key:13,role:"menuitem",target:"_self",href:e.safeUrl("/api/repo/"+e.repoId+"/zip")},[...t[63]||(t[63]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-archive"})],-1),y("Download ZIP",-1)])],8,_U)):f("v-if",!0),n("button",{type:"button",role:"menuitem",onClick:t[18]||(t[18]=a=>e.copyPath())},[...t[64]||(t[64]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-link"})],-1),y("Copy file path",-1)])]),t[65]||(t[65]=n("div",{class:"tb-menu-sep"},null,-1)),t[66]||(t[66]=n("a",{role:"menuitem",href:"/faq"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-help"})]),y("FAQ")],-1)),t[67]||(t[67]=n("a",{role:"menuitem",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",target:"_blank",rel:"noopener"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-bug"})]),y("Report a bug")],-1))])):f("v-if",!0)])]),e.gotoOpen&&e.type!="html"?(l(),d("form",{key:2,class:"goto-pop",role:"dialog","aria-label":"Go to line",onSubmit:t[22]||(t[22]=ce(a=>e.submitGoto(),["prevent"]))},[t[68]||(t[68]=n("label",{class:"goto-label",for:"goto-input"},"Go to line",-1)),A(n("input",{id:"goto-input",class:"goto-input",type:"text",inputmode:"numeric",autocomplete:"off",spellcheck:"false",placeholder:"1\u2013"+e.fmt?.number(typeof e.content=="string"?e.content.split(`
`).length:1),"aria-invalid":e.gotoError?"true":"false","onUpdate:modelValue":t[20]||(t[20]=a=>e.gotoValue=a),onKeydown:t[21]||(t[21]=At(ce(a=>e.closeGoto(),["prevent","stop"]),["esc"]))},null,40,OU),[[ct,e.gotoValue]]),n("span",{class:P(["goto-hint",{"is-error":e.gotoError}])},c(e.gotoError||"Line, or line:column"),3)],32)):f("v-if",!0)]),e.type!="html"?A((l(),d("div",IU,null,512)),[[r,{content:e.content,options:e.aceOption}]]):f("v-if",!0),e.type=="html"&&e.mdFindOpen?(l(),d("div",MU,[n("input",{type:"search",class:"md-find-input",placeholder:"Find on this page","aria-label":"Find on this page",value:e.mdFindQuery,onInput:t[23]||(t[23]=a=>{e.mdFindQuery=a.target.value,e.runMdFind()}),onKeydown:[t[24]||(t[24]=At(ce(a=>a.shiftKey?e.stepMdFind(-1):e.stepMdFind(1),["prevent"]),["enter"])),t[25]||(t[25]=At(ce(a=>e.toggleMdFind(!1),["prevent"]),["esc"]))]},null,40,VU),n("span",qU,[n("button",{type:"button","aria-label":"Previous match",onClick:t[26]||(t[26]=a=>e.stepMdFind(-1)),disabled:!e.mdFindTotal},[...t[69]||(t[69]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-left"})],-1)])],8,$U),n("button",{type:"button","aria-label":"Next match",onClick:t[27]||(t[27]=a=>e.stepMdFind(1)),disabled:!e.mdFindTotal},[...t[70]||(t[70]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-right"})],-1)])],8,LU)]),n("span",UU,c(e.mdFindQuery?e.mdFindTotal?e.mdFindIndex+1+" of "+e.mdFindTotal:"No results":""),1),n("button",{type:"button",class:P(["md-find-opt",{"is-on":e.mdFindCase}]),"aria-pressed":e.mdFindCase?"true":"false",title:"Match case","aria-label":"Match case",onClick:t[28]||(t[28]=a=>{e.mdFindCase=!e.mdFindCase,e.runMdFind()})},"Aa",10,FU),n("button",{type:"button",class:"md-find-close","aria-label":"Close find",onClick:t[29]||(t[29]=a=>e.toggleMdFind(!1))},"\xD7")])):f("v-if",!0),e.type=="html"?(l(),d("div",BU,[n("div",{class:"file-content markdown-body",innerHTML:e.renderReadme(e.sanitize(e.content))},null,8,zU),e.readmeToc().length>=3?(l(),d("aside",HU,[t[71]||(t[71]=n("div",{class:"readme-toc-head"},"On this page",-1)),n("nav",jU,[n("a",{href:"#",class:P(["lvl-2 toc-top",{"is-active":!e.activeHeading}]),onClick:t[30]||(t[30]=ce(a=>e.scrollToHeading(null),["prevent"]))},"Introduction",2),(l(!0),d(B,null,ie(e.readmeToc(),(a,u)=>(l(),d("a",{key:a.id,href:"#"+a.id,class:P(["lvl-"+a.level,{"is-active":e.activeHeading===a.id}]),onClick:ce(p=>e.scrollToHeading(a.id),["prevent"])},c(a.text),11,GU))),128))])])):f("v-if",!0)])):f("v-if",!0)],2)):f("v-if",!0),e.type=="html-doc"&&!e.showSource&&e.content!=null?(l(),sn(o,{key:2,content:e.content,"base-url":e.fileBaseUrl,"allow-scripts":e.allowScripts},null,8,["content","base-url","allow-scripts"])):f("v-if",!0),e.type=="image"?(l(),d("img",{key:3,class:"image-content",src:e.safeUrl(e.url)},null,8,WU)):f("v-if",!0),e.type=="media"?(l(),d("iframe",{key:4,class:"h-100 overflow-auto w-100 b-0",src:e.safeUrl(e.url)},null,8,KU)):f("v-if",!0),e.type=="pdf"?(l(),sn(i,{key:5,src:e.safeUrl(e.url)},null,8,["src"])):f("v-if",!0),e.type=="audio"?(l(),d("div",YU,[n("audio",XU,[n("source",{src:e.safeUrl(e.url)},null,8,JU)])])):f("v-if",!0),e.type=="IPython"?(l(),d("div",QU,[Ue(s,{file:e.url,content:e.content},null,8,["file","content"])])):f("v-if",!0),e.type=="rate_limited"?(l(),d("div",ZU,[n("div",xU,[t[74]||(t[74]=ke('<div class="paper-ratelimit-head"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg><div><div class="paper-error-eyebrow">Temporarily paused</div><div class="paper-error-title">GitHub API rate limit reached</div></div></div>',1)),n("p",eF,[t[72]||(t[72]=y("This repository will be available in ",-1)),n("strong",null,c(e.rateLimitCountdown),1),t[73]||(t[73]=y(". The page will reload automatically.",-1))])])])):f("v-if",!0),e.type=="error"?(l(),d("div",tF,[n("section",nF,[n("h1",oF,c(e.fmt.translate(e.isSourceAccessError()?"VIEWER_ERRORS.access_title":"VIEWER_ERRORS.title")),1),n("p",null,c(e.fmt.translate("ERRORS."+e.content)),1),e.isSourceAccessError()?(l(),d(B,{key:0},[n("p",null,c(e.fmt.translate("VIEWER_ERRORS.reader_help")),1),n("p",null,c(e.fmt.translate("VIEWER_ERRORS.owner_help")),1)],64)):f("v-if",!0),n("div",sF,[e.isSourceAccessError()?f("v-if",!0):(l(),d("a",iF,"Back to home")),e.isSourceAccessError()?f("v-if",!0):(l(),d("a",rF,"Read the FAQ")),n("button",{type:"button",class:"btn btn-primary",onClick:t[31]||(t[31]=a=>e.retryContent())},c(e.fmt.translate("VIEWER_ERRORS.retry")),1),e.isSourceAccessError()?(l(),d("a",aF,c(e.fmt.translate("VIEWER_ERRORS.dashboard")),1)):f("v-if",!0)])])])):f("v-if",!0),e.type=="loading"&&!e.error?(l(),d("section",lF,[n("div",dF,[n("strong",null,c(e.fmt.translate(e.fileReceivedBytes?"VIEWER_LOADING.receiving":"VIEWER_LOADING.loading")),1),n("span",uF,c(e.filePath),1),e.fileLoadingSlow?(l(),d("p",cF,c(e.fmt.translate("VIEWER_LOADING.slow")),1)):f("v-if",!0)]),n("div",pF,[(l(),d(B,null,ie(10,a=>n("span",{key:a})),64))])])):f("v-if",!0),e.type== Line truncated
`).length+1)),placeholder:`Jane Smith
MIT CSAIL
jane.smith@example.org`},null,8,l7),[[i,{state:e.viewState,set:r=>{e.terms=r},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),n("details",d7,[t[25]||(t[25]=n("summary",null,"Regex & replacement syntax",-1)),n("p",null,[t[21]||(t[21]=y("Matches become ",-1)),n("code",null,p(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[22]||(t[22]=y(". Use ",-1)),t[23]||(t[23]=n("code",null,"term=>replacement",-1)),t[24]||(t[24]=y(" for a custom replacement. Regex is supported.",-1))])]),T(n("div",u7,"Terms are in an invalid format.",512),[[J,e.defaultsForm?.terms?.errors?.format]])])]),n("section",c7,[t[35]||(t[35]=n("h2",null,"Reviewer display",-1)),n("div",null,[n("div",p7,[n("label",f7,[T(n("input",h7,null,512),[[i,{state:e.viewState,set:r=>{e.options.link=r},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[27]||(t[27]=y("Keep links",-1))]),n("label",m7,[T(n("input",g7,null,512),[[i,{state:e.viewState,set:r=>{e.options.notebook=r},value:e.options?.notebook,form:"defaultsForm",options:{}}]]),t[28]||(t[28]=y("Render notebooks",-1))]),n("label",v7,[T(n("input",y7,null,512),[[i,{state:e.viewState,set:r=>{e.options.image=r},value:e.options?.image,form:"defaultsForm",options:{}}]]),t[29]||(t[29]=y("Show images",-1))]),n("label",b7,[T(n("input",w7,null,512),[[i,{state:e.viewState,set:r=>{e.options.pdf=r},value:e.options?.pdf,form:"defaultsForm",options:{}}]]),t[30]||(t[30]=y("Show PDFs",-1))]),n("label",k7,[T(n("input",C7,null,512),[[i,{state:e.viewState,set:r=>{e.options.loc=r},value:e.options?.loc,form:"defaultsForm",options:{}}]]),t[31]||(t[31]=y("Show line counts",-1))]),n("label",E7,[T(n("input",S7,null,512),[[i,{state:e.viewState,set:r=>{e.options.page=r},value:e.options?.page,form:"defaultsForm",options:{}}]]),t[32]||(t[32]=y("GitHub Pages",-1))])]),t[33]||(t[33]=n("p",{class:"settings-help"},"Images and PDFs are not anonymized.",-1)),t[34]||(t[34]=n("p",{class:"settings-help"},"GitHub Pages must use the anonymized branch.",-1))])]),n("section",N7,[t[38]||(t[38]=n("h2",null,"Source updates",-1)),n("div",null,[n("label",R7,[T(n("input",D7,null,512),[[i,{state:e.viewState,set:r=>{e.options.update=r},value:e.options?.update,form:"defaultsForm",options:{}}]]),t[36]||(t[36]=y("Auto-update from GitHub",-1))]),t[37]||(t[37]=n("p",{class:"settings-help"},"Latest commit, at most once an hour.",-1))])]),n("section",T7,[t[40]||(t[40]=n("label",{class:"settings-label",for:"expiration"},"On expiration",-1)),n("div",null,[T((l(),d("select",A7,[...t[39]||(t[39]=[n("option",{value:"redirect"},"Redirect visitors to the GitHub repository",-1),n("option",{value:"remove",selected:""},"Remove the anonymized repository",-1)])])),[[i,{state:e.viewState,set:r=>{e.options.expirationMode=r},value:e.options?.expirationMode,form:"defaultsForm",options:{}}]]),n("p",_7,p(e.options.expirationMode==="redirect"?"Redirecting reveals the original repository. Choose the date per anonymization.":"Set the expiration date for each repository."),1)])]),n("footer",O7,[n("div",P7,[e.draftRestored?(l(),d("span",I7,"Your unsaved draft was restored.")):e.unsavedChanges?(l(),d("span",M7,"Unsaved changes"+p(e.draftSaved?" \xB7 Draft saved in this tab":""),1)):(l(),d("span",V7,p(e.message?"Defaults saved":"All changes saved"),1))]),e.draftRestored||e.unsavedChanges?(l(),d("button",{key:0,type:"button",class:"draft-discard",onClick:t[0]||(t[0]=r=>e.discardDraft())},"Discard changes")):f("v-if",!0),n("button",{id:"save",type:"submit",class:"btn btn-ink",disabled:e.saving},p(e.saving?"Saving\u2026":"Save defaults"),9,q7),e.error?(l(),d("div",$7,p(e.error),1)):f("v-if",!0)])],32)),[[o,e.viewState]])],512),[[J,e.settingsTab==="defaults"]]),T(n("section",{id:"settings-billing","aria-busy":e.billingLoading||e.billingBusy},[t[64]||(t[64]=n("p",{class:"settings-scope"},"Your plan and usage. Reviewer access is always free.",-1)),e.billingLoading?(l(),d("p",U7,"Loading billing\u2026")):f("v-if",!0),e.billingError?(l(),d("div",F7,[y(p(e.billingError)+" ",1),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[2]||(t[2]=r=>e.loadBilling()),disabled:e.billingBusy},"Try again",8,B7)])):f("v-if",!0),e.billingNotice?(l(),d("p",z7,p(e.billingNotice),1)):f("v-if",!0),e.billingPending?(l(),d("div",H7,[t[42]||(t[42]=n("strong",null,"Confirming your payment",-1)),t[43]||(t[43]=n("p",null,"Your allowance will update after confirmation. Please do not pay again.",-1)),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[3]||(t[3]=r=>e.refreshBilling()),disabled:e.billingBusy},"Check payment status",8,j7)])):f("v-if",!0),n("section",G7,[t[57]||(t[57]=n("h2",null,"Current plan",-1)),n("div",null,[n("div",W7,[n("div",null,[n("strong",null,p(e.profileAuthorGrant()?"Author subscription":e.billing?.enabled?"Free":"Account allowance"),1),e.profileAuthorGrant()?(l(),d("p",K7,p(e.profileAuthorGrant().cancelAtPeriodEnd?"Renewal cancelled \xB7 Access until ":"Paid access through ")+p(e.billingDate(e.profileAuthorGrant().until)),1)):f("v-if",!0)]),e. Line truncated
`);for(let v=0;v<g.length;v++){let h=g[v];if(h.startsWith("diff --git")){m(),c();continue}if(h.startsWith("--- ")){r&&r.lines.length&&m(),c().oldPath=h.replace(/^--- (a\/)?/,"").trim();continue}if(h.startsWith("+++ ")){c().newPath=h.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(h.startsWith("index ")||h.startsWith("similarity index")||h.startsWith("rename ")||h.startsWith("new file mode")||h.startsWith("deleted file mode")||h.startsWith("Binary files"))){if(h.startsWith("@@")){let k=h.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);k&&(a=parseInt(k[1],10),u=parseInt(k[2],10)),c().lines.push({kind:"hunk",oldNo:"",newNo:"",text:h});continue}r&&(h.startsWith("+")?(r.lines.push({kind:"add",oldNo:"",newNo:u,text:h.slice(1)}),u++):h.startsWith("-")?(r.lines.push({kind:"remove",oldNo:a,newNo:"",text:h.slice(1)}),a++):(r.lines.push({kind:"ctx",oldNo:a,newNo:u,text:h.startsWith(" ")?h.slice(1):h}),a++,u++))}}return s(o,r),o.join("")}})({trustAsHtml:e=>e});function aH(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function lH(e){return String(e??"").toUpperCase()}function dH(e,t,s=0){return(e||[]).slice(s,s+t)}function uH(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function cH(e,t){if(!t)return e||[];let s=Array.isArray(t)?t:[t];return[...e||[]].sort((i,o)=>{for(let r of s){let a=r[0]==="-"?-1:1;r=r.replace(/^[+-]/,"");let u=r.split(".").reduce((m,g)=>m?.[g],i),c=r.split(".").reduce((m,g)=>m?.[g],o);if(typeof u=="string"&&(u=u.toLowerCase()),typeof c=="string"&&(c=c.toLowerCase()),u!==c)return(u==null?-1:c==null?1:u<c?-1:1)*a}return 0})}function pH(e,t){if(!e)return"";let s=new Date(e);if(isNaN(s))return String(e);if(!t||t==="mediumDate")return s.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let i={yyyy:s.getFullYear(),MMM:s.toLocaleDateString(void 0,{month:"short"}),MM:String(s.getMonth()+1).padStart(2,"0"),dd:String(s.getDate()).padStart(2,"0"),d:s.getDate(),HH:String(s.getHours()).padStart(2,"0"),mm:String(s.getMinutes()).padStart(2,"0"),ss:String(s.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,o=>i[o])}function fH(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var zc={name:"htmlDoc",props:["content","baseUrl","allowScripts"],setup(e){let t=gt(null);return nn(()=>{let i=[t.value][0];i.classList.add("html-doc");function o(){i.innerHTML="";let r=e.content;if(typeof r!="string")return;let a=document.createElement("iframe");a.className="html-doc-frame",a.setAttribute("title","Rendered HTML document");let u=["allow-popups","allow-popups-to-escape-sandbox"];e.allowScripts&&u.push("allow-scripts","allow-forms","allow-modals"),a.setAttribute("sandbox",u.join(" ")),a.setAttribute("referrerpolicy","no-referrer"),i.appendChild(a);let c="";e.baseUrl&&(c='<base href="'+e.baseUrl.replace(/&/g,"&amp;").replace(/"/g,"&quot;")+'">'),a.srcdoc=c+r}tt(()=>e.content,o,{immediate:!0}),tt(()=>e.baseUrl,o,{immediate:!0}),tt(()=>e.allowScripts,o,{immediate:!0})}),()=>Oe("html-doc",{ref:t})}};var Hc={name:"pdfviewer",props:["src"],setup(e){let t=gt(null);return nn(()=>{let s=[t.value],i=400,o=[.5,.75,1,1.25,1.5,2,3],r=1e3,a=s[0];a.classList.add("pdf-viewer");let u=null,c=[],m=!1,g=null,v=1,h=1,k=0,A=0,w=612/792,b=0,E=null,N=new Map,O=0;function q(C){let R=N.get(C);R&&(R.cancelled=!0,R.task?.cancel?.(),N.delete(C));let P=C.querySelector("canvas");P&&(P.width=0,P.height=0),C.innerHTML="",delete C.dataset.state}let I=document.createElement("div");I.className="pdf-toolbar";let j=document.createElement("div");j.className="pdf-pages",j.tabIndex=0,j.setAttribute("aria-label","PDF pages"),a.appendChild(I),a.appendChild(j);function de(C,R,P){let G=document.createElement("button");return G.type="button",G.className="pdf-toolbar-btn",G.title=R,G.setAttribute("aria-label",R),G.innerHTML='<i class="fas '+C+'"></i>',G.addEventListener("click",P),G}let ie=de("fa-chevron-up","Previous page",function(){Ke(h-1)}),D=de("fa-chevron-down","Next page",function(){Ke(h+1)}),X=document.createElement("input");X.type="text",X.className="pdf-toolbar-page",X.setAttribute("aria-label","Page number"),X.title="Page number \u2014 type a page and press Enter",X.addEventListener("keydown",function(C){C.key==="Enter"&&(Ke(parseInt(X.value,10)),X.blur())}),X.addEventListener("blur",function(){X.value=String(h)});let le=document.createElement("span");le.className="pdf-toolbar-count";let Ee=de("fa-search-minus","Zoom out",function(){ue(o[Math.max(0,Ne()-1)])}),re=de("fa-search-plus","Zoom in",function(){ue(o[Math.min(o.length-1,Ne()+1)])}),me=document.createElement("button");me.type="button",me.className="pdf-toolbar-btn pdf-toolbar-zoom",me.title="Reset zoom to fit the width",me.setAttribute("aria-label","Reset zoom to fit the width"),me.addEventListener("click",function(){ue(1)}),I.appendChild(ie),I.appendChild(D);let ke=document.createElement("span");ke.className="pdf-toolbar-group",ke.append Line truncated
jane.smith@example.org`},null,8,RB),[[i,{state:e.viewState,set:r=>{e.terms=r},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),n("details",DB,[t[25]||(t[25]=n("summary",null,"Regex & replacement syntax",-1)),n("p",null,[t[21]||(t[21]=y("Matches become ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[22]||(t[22]=y(". Use ",-1)),t[23]||(t[23]=n("code",null,"term=>replacement",-1)),t[24]||(t[24]=y(" for a custom replacement. Regex is supported.",-1))])]),A(n("div",AB,"Terms are in an invalid format.",512),[[J,e.defaultsForm?.terms?.errors?.format]])])]),n("section",TB,[t[35]||(t[35]=n("h2",null,"Reviewer display",-1)),n("div",null,[n("div",PB,[n("label",_B,[A(n("input",OB,null,512),[[i,{state:e.viewState,set:r=>{e.options.link=r},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[27]||(t[27]=y("Keep links",-1))]),n("label",IB,[A(n("input",MB,null,512),[[i,{state:e.viewState,set:r=>{e.options.notebook=r},value:e.options?.notebook,form:"defaultsForm",options:{}}]]),t[28]||(t[28]=y("Render notebooks",-1))]),n("label",VB,[A(n("input",qB,null,512),[[i,{state:e.viewState,set:r=>{e.options.image=r},value:e.options?.image,form:"defaultsForm",options:{}}]]),t[29]||(t[29]=y("Show images",-1))]),n("label",$B,[A(n("input",LB,null,512),[[i,{state:e.viewState,set:r=>{e.options.pdf=r},value:e.options?.pdf,form:"defaultsForm",options:{}}]]),t[30]||(t[30]=y("Show PDFs",-1))]),n("label",UB,[A(n("input",FB,null,512),[[i,{state:e.viewState,set:r=>{e.options.loc=r},value:e.options?.loc,form:"defaultsForm",options:{}}]]),t[31]||(t[31]=y("Show line counts",-1))]),n("label",BB,[A(n("input",zB,null,512),[[i,{state:e.viewState,set:r=>{e.options.page=r},value:e.options?.page,form:"defaultsForm",options:{}}]]),t[32]||(t[32]=y("GitHub Pages",-1))])]),t[33]||(t[33]=n("p",{class:"settings-help"},"Images and PDFs are not anonymized.",-1)),t[34]||(t[34]=n("p",{class:"settings-help"},"GitHub Pages must use the anonymized branch.",-1))])]),n("section",HB,[t[38]||(t[38]=n("h2",null,"Source updates",-1)),n("div",null,[n("label",jB,[A(n("input",GB,null,512),[[i,{state:e.viewState,set:r=>{e.options.update=r},value:e.options?.update,form:"defaultsForm",options:{}}]]),t[36]||(t[36]=y("Auto-update from GitHub",-1))]),t[37]||(t[37]=n("p",{class:"settings-help"},"Latest commit, at most once an hour.",-1))])]),n("section",WB,[t[40]||(t[40]=n("label",{class:"settings-label",for:"expiration"},"On expiration",-1)),n("div",null,[A((l(),d("select",KB,[...t[39]||(t[39]=[n("option",{value:"redirect"},"Redirect visitors to the GitHub repository",-1),n("option",{value:"remove",selected:""},"Remove the anonymized repository",-1)])])),[[i,{state:e.viewState,set:r=>{e.options.expirationMode=r},value:e.options?.expirationMode,form:"defaultsForm",options:{}}]]),n("p",YB,c(e.options.expirationMode==="redirect"?"Redirecting reveals the original repository. Choose the date per anonymization.":"Set the expiration date for each repository."),1)])]),n("footer",XB,[n("div",JB,[e.draftRestored?(l(),d("span",QB,"Your unsaved draft was restored.")):e.unsavedChanges?(l(),d("span",ZB,"Unsaved changes"+c(e.draftSaved?" \xB7 Draft saved in this tab":""),1)):(l(),d("span",xB,c(e.message?"Defaults saved":"All changes saved"),1))]),e.draftRestored||e.unsavedChanges?(l(),d("button",{key:0,type:"button",class:"draft-discard",onClick:t[0]||(t[0]=r=>e.discardDraft())},"Discard changes")):f("v-if",!0),n("button",{id:"save",type:"submit",class:"btn btn-ink",disabled:e.saving},c(e.saving?"Saving\u2026":"Save defaults"),9,e7),e.error?(l(),d("div",t7,c(e.error),1)):f("v-if",!0)])],32)),[[s,e.viewState]])],512),[[J,e.settingsTab==="defaults"]]),A(n("section",{id:"settings-billing","aria-busy":e.billingLoading||e.billingBusy},[t[95]||(t[95]=n("p",{class:"settings-scope"},"Your plan and usage. Reviewer access is always free.",-1)),e.billingLoading?(l(),d("p",o7,"Loading billing\u2026")):f("v-if",!0),e.billingError?(l(),d("div",s7,[y(c(e.billingError)+" ",1),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[2]||(t[2]=r=>e.loadBilling()),disabled:e.billingBusy},"Try again",8,i7)])):f("v-if",!0),e.billingNotice?(l(),d("p",r7,c(e.billingNotice),1)):f("v-if",!0),e.billingPending?(l(),d("div",a7,[t[42]||(t[42]=n("strong",null,"Confirming your payment",-1)),t[43]||(t[43]=n("p",null,"Your allowance will update after confirmation. Please do not pay again.",-1)),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[3]||(t[3]=r=>e.refreshBilling()),disabled:e.billingBusy},"Check payment status",8,l7)])):f("v-if",!0),n("section",d7,[n("div",u7,[n("div",null,[t[44]||(t[44]=n("p",{class:"billing-eyebrow"},"Current plan",-1)),n("h2",c7,c(e.profileAuthorGrant()?e.profileGrantName(e.profileAuthorGrant()):e.billing?.enabled?"Free":"Account allowance"),1),e.profileAuthorGrant()?(l(),d("p",p7,[y(c(e.profileAuthorGrant().cancelAtPeriodEnd?"Renewal cancelled \xB7 access until ":"Renews ")+c(e.billingDate(e.profileAuthorGrant().until)),1),e.profi Line truncated
`);for(let g=0;g<h.length;g++){let m=h[g];if(m.startsWith("diff --git")){v(),p();continue}if(m.startsWith("--- ")){r&&r.lines.length&&v(),p().oldPath=m.replace(/^--- (a\/)?/,"").trim();continue}if(m.startsWith("+++ ")){p().newPath=m.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(m.startsWith("index ")||m.startsWith("similarity index")||m.startsWith("rename ")||m.startsWith("new file mode")||m.startsWith("deleted file mode")||m.startsWith("Binary files"))){if(m.startsWith("@@")){let k=m.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);k&&(a=parseInt(k[1],10),u=parseInt(k[2],10)),p().lines.push({kind:"hunk",oldNo:"",newNo:"",text:m});continue}r&&(m.startsWith("+")?(r.lines.push({kind:"add",oldNo:"",newNo:u,text:m.slice(1)}),u++):m.startsWith("-")?(r.lines.push({kind:"remove",oldNo:a,newNo:"",text:m.slice(1)}),a++):(r.lines.push({kind:"ctx",oldNo:a,newNo:u,text:m.startsWith(" ")?m.slice(1):m}),a++,u++))}}return o(s,r),s.join("")}})({trustAsHtml:e=>e});function h9(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function m9(e){return String(e??"").toUpperCase()}function g9(e,t,o=0){return(e||[]).slice(o,o+t)}function v9(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function y9(e,t){if(!t)return e||[];let o=Array.isArray(t)?t:[t];return[...e||[]].sort((i,s)=>{for(let r of o){let a=r[0]==="-"?-1:1;r=r.replace(/^[+-]/,"");let u=r.split(".").reduce((v,h)=>v?.[h],i),p=r.split(".").reduce((v,h)=>v?.[h],s);if(typeof u=="string"&&(u=u.toLowerCase()),typeof p=="string"&&(p=p.toLowerCase()),u!==p)return(u==null?-1:p==null?1:u<p?-1:1)*a}return 0})}function b9(e,t){if(!e)return"";let o=new Date(e);if(isNaN(o))return String(e);if(!t||t==="mediumDate")return o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let i={yyyy:o.getFullYear(),MMM:o.toLocaleDateString(void 0,{month:"short"}),MM:String(o.getMonth()+1).padStart(2,"0"),dd:String(o.getDate()).padStart(2,"0"),d:o.getDate(),HH:String(o.getHours()).padStart(2,"0"),mm:String(o.getMinutes()).padStart(2,"0"),ss:String(o.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,s=>i[s])}function w9(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var jc={name:"htmlDoc",props:["content","baseUrl","allowScripts"],setup(e){let t=gt(null);return on(()=>{let i=[t.value][0];i.classList.add("html-doc");function s(){i.innerHTML="";let r=e.content;if(typeof r!="string")return;let a=document.createElement("iframe");a.className="html-doc-frame",a.setAttribute("title","Rendered HTML document");let u=["allow-popups","allow-popups-to-escape-sandbox"];e.allowScripts&&u.push("allow-scripts","allow-forms","allow-modals"),a.setAttribute("sandbox",u.join(" ")),a.setAttribute("referrerpolicy","no-referrer"),i.appendChild(a);let p="";e.baseUrl&&(p='<base href="'+e.baseUrl.replace(/&/g,"&amp;").replace(/"/g,"&quot;")+'">'),a.srcdoc=p+r}tt(()=>e.content,s,{immediate:!0}),tt(()=>e.baseUrl,s,{immediate:!0}),tt(()=>e.allowScripts,s,{immediate:!0})}),()=>_e("html-doc",{ref:t})}};var Gc={name:"pdfviewer",props:["src"],setup(e){let t=gt(null);return on(()=>{let o=[t.value],i=400,s=[.5,.75,1,1.25,1.5,2,3],r=1e3,a=o[0];a.classList.add("pdf-viewer");let u=null,p=[],v=!1,h=null,g=1,m=1,k=0,T=0,w=612/792,b=0,E=null,N=new Map,O=0;function q(C){let R=N.get(C);R&&(R.cancelled=!0,R.task?.cancel?.(),N.delete(C));let I=C.querySelector("canvas");I&&(I.width=0,I.height=0),C.innerHTML="",delete C.dataset.state}let M=document.createElement("div");M.className="pdf-toolbar";let G=document.createElement("div");G.className="pdf-pages",G.tabIndex=0,G.setAttribute("aria-label","PDF pages"),a.appendChild(M),a.appendChild(G);function de(C,R,I){let W=document.createElement("button");return W.type="button",W.className="pdf-toolbar-btn",W.title=R,W.setAttribute("aria-label",R),W.innerHTML='<i class="fas '+C+'"></i>',W.addEventListener("click",I),W}let re=de("fa-chevron-up","Previous page",function(){Ke(m-1)}),D=de("fa-chevron-down","Next page",function(){Ke(m+1)}),X=document.createElement("input");X.type="text",X.className="pdf-toolbar-page",X.setAttribute("aria-label","Page number"),X.title="Page number \u2014 type a page and press Enter",X.addEventListener("keydown",function(C){C.key==="Enter"&&(Ke(parseInt(X.value,10)),X.blur())}),X.addEventListener("blur",function(){X.value=String(m)});let le=document.createElement("span");le.className="pdf-toolbar-count";let Ee=de("fa-search-minus","Zoom out",function(){ue(s[Math.max(0,Ne()-1)])}),ae=de("fa-search-plus","Zoom in",function(){ue(s[Math.min(s.length-1,Ne()+1)])}),he=document.createElement("button");he.type="button",he.className="pdf-toolbar-btn pdf-toolbar-zoom",he.title="Reset zoom to fit the width",he.setAttribute("aria-label","Reset zoom to fit the width"),he.addEventListener("click",function(){ue(1)}),M.appendChild(re),M.appendChild(D);let Ce=document.createElement("span");Ce.className="pdf-toolbar-group",Ce.append Line truncated
/*! Bundled license information:
@vue/shared/dist/shared.esm-bundler.js:
+3 -3
View File
@@ -44,7 +44,7 @@ async function main() {
products.push(product.id);
for (const [key, amount, interval] of [
["STRIPE_AUTHOR_MONTHLY_PRICE",500,"month"],["STRIPE_AUTHOR_ANNUAL_PRICE",4900,"year"],
["STRIPE_AUTHOR_PASS_PRICE",1500],["STRIPE_CONFERENCE_PRICE",19900]]) {
["STRIPE_AUTHOR_PASS_PRICE",400],["STRIPE_REPO_EXTEND_PRICE",200],["STRIPE_REPO_NEVER_PRICE",1000],["STRIPE_REPO_SLOT_PRICE",200],["STRIPE_REPO_PACK_PRICE",500],["STRIPE_CONFERENCE_SMALL_PRICE",4900],["STRIPE_CONFERENCE_PRICE",14900],["STRIPE_CONFERENCE_LARGE_PRICE",39900]]) {
const p = await stripe.prices.create({ product: product.id, currency: "eur", unit_amount: amount, metadata, ...(interval ? { recurring: { interval } } : {}) });
prices.push(p.id); config[key] = p.id;
}
@@ -52,9 +52,9 @@ async function main() {
await Repo.create({ repoId: "sandbox-"+run, owner: buyer.owner, status: "ready" });
await Conference.create({ conferenceID: "sandbox-"+run, owners: [buyer.owner], status: "pending_payment",
startDate: new Date(), endDate: new Date(Date.now()+86400000*30), plan: { planID: "conference" } });
for (const plan of ["author_monthly","author_annual","author_pass","conference"]) {
for (const plan of ["author_monthly","author_annual","author_pass","repo_extend","repo_never","repo_slot","repo_pack","conference"]) {
await check(plan + " Checkout creation, reuse, unpaid denial and expiry", async () => {
const target = ["author_pass","conference"].includes(plan) ? "sandbox-"+run : "";
const target = ["author_pass","repo_extend","repo_never","repo_slot","repo_pack","conference"].includes(plan) ? "sandbox-"+run : "";
const url = await createCheckout(buyer.owner, plan, target, stripe);
assert.match(url, /^https:\/\/checkout.stripe.com\//);
assert.equal(await createCheckout(buyer.owner,plan,target,stripe), url);
+29 -1
View File
@@ -12,6 +12,14 @@ interface Config {
STRIPE_AUTHOR_ANNUAL_PRICE: string;
STRIPE_AUTHOR_PASS_PRICE: string;
STRIPE_CONFERENCE_PRICE: string;
STRIPE_CONFERENCE_SMALL_PRICE: string;
STRIPE_CONFERENCE_LARGE_PRICE: string;
STRIPE_LEGACY_PRICES: string;
STRIPE_REPO_EXTEND_PRICE: string;
STRIPE_REPO_NEVER_PRICE: string;
STRIPE_REPO_SLOT_PRICE: string;
STRIPE_REPO_PACK_PRICE: string;
FREE_EXPIRATION_MONTHS: number;
FREE_AUTHOR_REPOSITORIES: number;
FREE_AUTHOR_STORAGE: number;
PAID_AUTHOR_REPOSITORIES: number;
@@ -19,6 +27,10 @@ interface Config {
PAID_PASS_STORAGE: number;
PAID_CONFERENCE_REPOSITORIES: number;
PAID_CONFERENCE_STORAGE: number;
PAID_CONFERENCE_SMALL_REPOSITORIES: number;
PAID_CONFERENCE_SMALL_STORAGE: number;
PAID_CONFERENCE_LARGE_REPOSITORIES: number;
PAID_CONFERENCE_LARGE_STORAGE: number;
RESEND_API_KEY: string;
EMAIL_FROM: string;
@@ -112,6 +124,15 @@ const config: Config = {
STRIPE_AUTHOR_ANNUAL_PRICE: "",
STRIPE_AUTHOR_PASS_PRICE: "",
STRIPE_CONFERENCE_PRICE: "",
STRIPE_CONFERENCE_SMALL_PRICE: "",
STRIPE_CONFERENCE_LARGE_PRICE: "",
// Retired price IDs that existing purchases still use: "price_old:author_annual,price_x:repo_pack".
STRIPE_LEGACY_PRICES: "",
STRIPE_REPO_EXTEND_PRICE: "",
STRIPE_REPO_NEVER_PRICE: "",
STRIPE_REPO_SLOT_PRICE: "",
STRIPE_REPO_PACK_PRICE: "",
FREE_EXPIRATION_MONTHS: 5,
FREE_AUTHOR_REPOSITORIES: 5,
FREE_AUTHOR_STORAGE: 2 * 1024 ** 3,
PAID_AUTHOR_REPOSITORIES: 20,
@@ -119,6 +140,10 @@ const config: Config = {
PAID_PASS_STORAGE: 2 * 1024 ** 3,
PAID_CONFERENCE_REPOSITORIES: 100,
PAID_CONFERENCE_STORAGE: 50 * 1024 ** 3,
PAID_CONFERENCE_SMALL_REPOSITORIES: 25,
PAID_CONFERENCE_SMALL_STORAGE: 10 * 1024 ** 3,
PAID_CONFERENCE_LARGE_REPOSITORIES: 400,
PAID_CONFERENCE_LARGE_STORAGE: 200 * 1024 ** 3,
RESEND_API_KEY: "",
EMAIL_FROM: "",
@@ -270,8 +295,11 @@ if (config.GITHUB_APP_ENABLED) {
export default config;
if (config.BILLING_ENABLED) {
for (const key of ["FREE_AUTHOR_REPOSITORIES", "FREE_AUTHOR_STORAGE", "PAID_AUTHOR_REPOSITORIES", "PAID_AUTHOR_STORAGE", "PAID_PASS_STORAGE", "PAID_CONFERENCE_REPOSITORIES", "PAID_CONFERENCE_STORAGE"] as const) {
for (const key of ["FREE_EXPIRATION_MONTHS", "FREE_AUTHOR_REPOSITORIES", "FREE_AUTHOR_STORAGE", "PAID_AUTHOR_REPOSITORIES", "PAID_AUTHOR_STORAGE", "PAID_PASS_STORAGE", "PAID_CONFERENCE_REPOSITORIES", "PAID_CONFERENCE_STORAGE", "PAID_CONFERENCE_SMALL_REPOSITORIES", "PAID_CONFERENCE_SMALL_STORAGE", "PAID_CONFERENCE_LARGE_REPOSITORIES", "PAID_CONFERENCE_LARGE_STORAGE"] as const) {
if (!Number.isSafeInteger(config[key]) || config[key] <= 0) throw new Error(`${key} must be a positive integer`);
}
if (!config.STRIPE_SECRET_KEY || !config.STRIPE_WEBHOOK_SECRET) throw new Error("Billing requires STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET");
for (const entry of config.STRIPE_LEGACY_PRICES.split(",").map(e => e.trim()).filter(Boolean)) {
if (!/^price_[A-Za-z0-9]+:[a-z_]+$/.test(entry)) throw new Error(`Invalid STRIPE_LEGACY_PRICES entry: ${entry}`);
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { REPOSITORY_ID_COLLATION } from "./repository-id";
import { allowance, reserveRepository } from "./billing/quotas";
import { reserveRepository } from "./billing/quotas";
import { hasAutomaticUpdates } from "./billing/expiration";
import { FILE_TREE_BATCH_SIZE, FILE_LISTING_LIMIT, deleteRowsInBatches } from "./file-tree";
import { SourceBase, sourceStorageSize } from "./source/Source";
import { notifyOwnerAccessProblem, withOwnerAccessRecovery } from "./owner-notifications";
@@ -724,7 +725,7 @@ export default class Repository {
// Reviewer visits must not trigger an unaffordable refresh after paid access ends.
// Free managed repositories can still be refreshed explicitly by their owner.
if (config.BILLING_ENABLED && this.model.billingManaged && !opt?.force &&
(await allowance(String(this.model.owner), this.repoId, this.model.conference)).plan === "free") return;
!(await hasAutomaticUpdates(String(this.model.owner), this.repoId, this.model.conference))) return;
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (
+36 -5
View File
@@ -1,16 +1,47 @@
import config from "../../config";
import AnonymousError from "../AnonymousError";
export type PlanId = "author_monthly" | "author_annual" | "author_pass" | "conference";
export type PlanId = "author_monthly" | "author_annual" | "author_pass" | "repo_extend" | "repo_never" | "repo_slot" | "repo_pack" | "conference_workshop" | "conference" | "conference_large";
// Repository add-ons bought once for a single repository.
export const REPOSITORY_ADDONS: PlanId[] = ["author_pass", "repo_extend", "repo_never", "repo_slot", "repo_pack"];
// A paper pack bundles what one paper needs during review into one checkout.
export const PACK_PLANS: PlanId[] = ["repo_slot", "author_pass", "repo_extend"];
// Capacity plans that do not include automatic updates.
export const MANUAL_REFRESH_PLANS = ["free", "repo_slot"];
// Conference review packages, smallest first. "conference" keeps its historical
// id for the middle tier so existing grants and conferences stay valid.
export const CONFERENCE_PLANS: PlanId[] = ["conference_workshop", "conference", "conference_large"];
export function isConferencePlan(id?: string | null) { return !!id && (CONFERENCE_PLANS as string[]).includes(id); }
export const REPO_EXTEND_MS = 365 * 86400_000;
export const REPO_NEVER_UNTIL = new Date("9999-12-31T00:00:00Z");
export const GiB = 1024 ** 3;
// Plans with `capacity` provide a storage allowance. The others only change expiration.
export function billingPlans() {
return [
{ id: "author_monthly" as PlanId, name: "Author monthly", priceId: config.STRIPE_AUTHOR_MONTHLY_PRICE, recurring: true, repositories: config.PAID_AUTHOR_REPOSITORIES, storage: config.PAID_AUTHOR_STORAGE, description: "Never-expiring repositories while subscribed, then a 90-day grace period. Renews monthly. Cancel renewal at any time." },
{ id: "author_annual" as PlanId, name: "Author annual", priceId: config.STRIPE_AUTHOR_ANNUAL_PRICE, recurring: true, repositories: config.PAID_AUTHOR_REPOSITORIES, storage: config.PAID_AUTHOR_STORAGE, description: "Never-expiring repositories while subscribed, then a 90-day grace period. Renews annually. Cancel renewal at any time." },
{ id: "author_pass" as PlanId, name: "Author review pass", priceId: config.STRIPE_AUTHOR_PASS_PRICE, recurring: false, repositories: 1, storage: config.PAID_PASS_STORAGE, description: "One repository for 180 days. No automatic renewal." },
{ id: "conference" as PlanId, name: "Conference review package", priceId: config.STRIPE_CONFERENCE_PRICE, recurring: false, repositories: config.PAID_CONFERENCE_REPOSITORIES, storage: config.PAID_CONFERENCE_STORAGE, description: "A review window of up to 180 days. No automatic renewal." },
{ id: "author_monthly" as PlanId, name: "Author monthly", priceId: config.STRIPE_AUTHOR_MONTHLY_PRICE, recurring: true, capacity: true, repositories: config.PAID_AUTHOR_REPOSITORIES, storage: config.PAID_AUTHOR_STORAGE, description: "Never-expiring repositories while subscribed, then a 90-day grace period. Renews monthly. Cancel renewal at any time." },
{ id: "author_annual" as PlanId, name: "Author annual", priceId: config.STRIPE_AUTHOR_ANNUAL_PRICE, recurring: true, capacity: true, repositories: config.PAID_AUTHOR_REPOSITORIES, storage: config.PAID_AUTHOR_STORAGE, description: "Never-expiring repositories while subscribed, then a 90-day grace period. Renews annually. Cancel renewal at any time." },
{ id: "author_pass" as PlanId, name: "Live updates", priceId: config.STRIPE_AUTHOR_PASS_PRICE, recurring: false, capacity: true, repositories: 1, storage: config.PAID_PASS_STORAGE, description: "Automatic updates for one repository for 180 days, with its own storage allowance. No automatic renewal." },
{ id: "repo_extend" as PlanId, name: "Extended expiration", priceId: config.STRIPE_REPO_EXTEND_PRICE, recurring: false, capacity: false, repositories: 0, storage: 0, description: "Keep one repository online for up to one more year. Buy again to add another year. No automatic renewal." },
{ id: "repo_never" as PlanId, name: "Never expire", priceId: config.STRIPE_REPO_NEVER_PRICE, recurring: false, capacity: false, repositories: 0, storage: 0, description: "One repository never expires. One-time payment, no subscription." },
{ id: "repo_slot" as PlanId, name: "Extra repository", priceId: config.STRIPE_REPO_SLOT_PRICE, recurring: false, capacity: true, repositories: 1, storage: config.PAID_PASS_STORAGE, description: "Host one more repository beyond the free allowance, with its own storage. It does not count toward your free repositories. One-time payment, valid while the repository exists." },
{ id: "repo_pack" as PlanId, name: "Paper pack", priceId: config.STRIPE_REPO_PACK_PRICE, recurring: false, capacity: false, repositories: 0, storage: 0, description: "Everything one paper needs during review: an extra repository, automatic updates for 180 days, and one more year before it expires. One-time payment." },
{ id: "conference_workshop" as PlanId, name: "Workshop", priceId: config.STRIPE_CONFERENCE_SMALL_PRICE, recurring: false, capacity: true, repositories: config.PAID_CONFERENCE_SMALL_REPOSITORIES, storage: config.PAID_CONFERENCE_SMALL_STORAGE, description: "For a workshop or small venue. A review window of up to 180 days. No automatic renewal." },
{ id: "conference" as PlanId, name: "Conference", priceId: config.STRIPE_CONFERENCE_PRICE, recurring: false, capacity: true, repositories: config.PAID_CONFERENCE_REPOSITORIES, storage: config.PAID_CONFERENCE_STORAGE, description: "For a typical conference track. A review window of up to 180 days. No automatic renewal." },
{ id: "conference_large" as PlanId, name: "Large conference", priceId: config.STRIPE_CONFERENCE_LARGE_PRICE, recurring: false, capacity: true, repositories: config.PAID_CONFERENCE_LARGE_REPOSITORIES, storage: config.PAID_CONFERENCE_LARGE_STORAGE, description: "For a large venue with several tracks. A review window of up to 180 days. No automatic renewal." },
];
}
// Retired Stripe prices keep their plan, so existing subscriptions and
// purchases stay recognized after a price ID is replaced.
export function legacyPricePlans() {
const plans = new Map<string, PlanId>();
const ids = new Set(billingPlans().map(p => p.id as string));
for (const entry of config.STRIPE_LEGACY_PRICES.split(",").map(e => e.trim()).filter(Boolean)) {
const [price, plan] = entry.split(":").map(part => part.trim());
if (!price || !plan || !ids.has(plan)) throw new Error(`Invalid STRIPE_LEGACY_PRICES entry: ${entry}`);
plans.set(price, plan as PlanId);
}
return plans;
}
export function billingError(code: string, status = 400) { return new AnonymousError(code, { httpStatus: status }); }
export function billingOrigin() {
const url = new URL(config.BILLING_ORIGIN);
+71 -15
View File
@@ -2,33 +2,80 @@ import { reconcileBillingAccount } from "./reconciliation";
import { createLogger } from "../logger";
import config from "../../config";
import { BillingGrant } from "./models";
import { billingError } from "./config";
import { usableGrant } from "./quotas";
import { billingError, MANUAL_REFRESH_PLANS } from "./config";
import { allowance, usableGrant } from "./quotas";
import Repo from "../model/anonymizedRepositories/anonymizedRepositories.model";
import ConferenceModel from "../model/conference/conferences.model";
export const NEVER_EXPIRATION_GRACE_MS = 90 * 86400000;
// A one-time Never expire add-on keeps a rolling deadline this far ahead, so a
// refund or dispute still ends it after the usual grace period.
const ROLLING_REFRESH_MS = 7 * 86400000;
async function authorGrants(owner: string) {
return BillingGrant.find({ scope: `user:${owner}`, planId: { $in: ["author_monthly", "author_annual"] } }).lean();
}
async function hasRepositoryGrant(owner: string, repoId: string, planId: string, now = new Date()) {
const grants = await BillingGrant.find({ scope: `repo:${repoId}`, owner, planId, revoked: false, until: { $gt: now } }).lean();
return grants.filter(g => usableGrant(g, now));
}
// Automatic updates come from the plan that funds the repository, or from the
// owner's author subscription even when a paid slot funds its storage.
export async function hasAutomaticUpdates(owner: string, repoId: string, conference?: string, now = new Date()) {
if (!MANUAL_REFRESH_PLANS.includes((await allowance(owner, repoId, conference)).plan)) return true;
return (await authorGrants(owner)).some(g => usableGrant(g, now));
}
// This is an author benefit, independent of which scope pays the storage quota.
export async function neverExpirationDeadline(owner: string, existing?: Date) {
// A repository's own Never expire add-on also qualifies.
export async function neverExpirationDeadline(owner: string, existing?: Date, repoId?: string) {
if (!config.BILLING_ENABLED) return undefined;
const now = new Date();
const grants = await authorGrants(owner);
const active = grants.filter(g => usableGrant(g));
if (!active.length) {
if (existing && existing > new Date()) return existing;
const active = grants.filter(g => usableGrant(g, now));
const addon = repoId ? (await hasRepositoryGrant(owner, repoId, "repo_never", now)).length > 0 : false;
if (!active.length && !addon) {
if (existing && existing > now) return existing;
throw billingError("subscription_required_for_never_expiration", 402);
}
return new Date(Math.max(existing?.getTime() || 0, ...active.map(g => g.until!.getTime() + NEVER_EXPIRATION_GRACE_MS)));
return new Date(Math.max(existing?.getTime() || 0, ...active.map(g => g.until!.getTime() + NEVER_EXPIRATION_GRACE_MS),
addon ? now.getTime() + NEVER_EXPIRATION_GRACE_MS : 0));
}
function addMonths(date: Date, months: number) {
const result = new Date(date);
result.setMonth(result.getMonth() + months);
result.setHours(23, 59, 59, 999);
return result;
}
// Free repositories can be scheduled FREE_EXPIRATION_MONTHS ahead. Author
// subscribers keep one year; an Extended expiration add-on sets its own limit.
export function freeExpirationLimit(now = new Date()) { return addMonths(now, config.FREE_EXPIRATION_MONTHS); }
export async function maximumExpiration(owner: string, repoId?: string, conference?: string, now = new Date()) {
if (!config.BILLING_ENABLED) return addMonths(now, 12);
let maximum = freeExpirationLimit(now).getTime();
// Conference submissions can stay online until the end of the review window.
const conf = conference && await ConferenceModel.findOne({ conferenceID: conference }).select("endDate").lean();
if (conf && conf.endDate) maximum = Math.max(maximum, new Date(conf.endDate).getTime());
if ((await authorGrants(owner)).some(g => usableGrant(g, now))) maximum = Math.max(maximum, addMonths(now, 12).getTime());
if (repoId) {
for (const grant of await hasRepositoryGrant(owner, repoId, "repo_extend", now)) {
const limit = new Date(grant.until!);
limit.setHours(23, 59, 59, 999);
maximum = Math.max(maximum, limit.getTime());
}
}
return new Date(maximum);
}
// Run before ordinary expiration maintenance. Legacy never-expiring links have
// no billing deadline and are deliberately excluded.
export async function reconcileNeverExpiration(now = new Date(), reconcile = reconcileBillingAccount) {
if (!config.BILLING_ENABLED) return;
const addons = new Set((await BillingGrant.find({ planId: "repo_never", revoked: false, until: { $gt: now } }).select("owner scope status until revoked").lean())
.filter(g => usableGrant(g, now)).map(g => `${g.owner} ${g.scope}`));
const cursor = Repo.find({ "options.expirationMode": "never", billingNeverExpiresUntil: { $exists: true },
status: { $nin: ["removed", "expired", "expiring", "removing"] } }).select("owner billingNeverExpiresUntil").cursor();
status: { $nin: ["removed", "expired", "expiring", "removing"] } }).select("owner repoId billingNeverExpiresUntil").cursor();
const deadlines = new Map<string, number>();
const verified = new Set<string>();
const deferred = new Set<string>();
@@ -40,7 +87,10 @@ export async function reconcileNeverExpiration(now = new Date(), reconcile = rec
.map(g => g.until!.getTime() + NEVER_EXPIRATION_GRACE_MS)));
}
const previous = repo.billingNeverExpiresUntil!;
let deadline = new Date(Math.max(previous.getTime(), deadlines.get(owner)!));
// Roll the add-on deadline forward weekly rather than on every run.
let rolling = addons.has(`${owner} repo:${repo.repoId}`) && previous.getTime() < now.getTime() + NEVER_EXPIRATION_GRACE_MS - ROLLING_REFRESH_MS
? now.getTime() + NEVER_EXPIRATION_GRACE_MS : 0;
let deadline = new Date(Math.max(previous.getTime(), deadlines.get(owner)!, rolling));
if (deadline <= now) {
if (deferred.has(owner)) continue;
try {
@@ -49,8 +99,13 @@ export async function reconcileNeverExpiration(now = new Date(), reconcile = rec
verified.add(owner);
const fresh = await authorGrants(owner);
deadlines.set(owner, Math.max(0, ...fresh.filter(g => !g.revoked && g.until).map(g => g.until!.getTime() + NEVER_EXPIRATION_GRACE_MS)));
// Reconciliation can recover a missed Never expire purchase too.
for (const grant of await BillingGrant.find({ owner, planId: "repo_never", revoked: false, until: { $gt: now } }).select("owner scope status until revoked").lean()) {
if (usableGrant(grant, now)) addons.add(`${grant.owner} ${grant.scope}`);
}
}
deadline = new Date(Math.max(previous.getTime(), deadlines.get(owner)!));
if (addons.has(`${owner} repo:${repo.repoId}`)) rolling = now.getTime() + NEVER_EXPIRATION_GRACE_MS;
deadline = new Date(Math.max(previous.getTime(), deadlines.get(owner)!, rolling));
} catch {
deferred.add(owner);
createLogger("billing").error("repository expiration deferred: billing verification failed", { owner });
@@ -67,12 +122,13 @@ export async function reconcileNeverExpiration(now = new Date(), reconcile = rec
}
}
export function validateBillingExpiration(options: { expirationMode?: string; expirationDate?: string | Date }) {
// `current` keeps an already saved date valid, so lowering the limit does not
// block unrelated edits to existing repositories.
export function validateBillingExpiration(options: { expirationMode?: string; expirationDate?: string | Date },
maximum = freeExpirationLimit(), current?: string | Date | null) {
if (!config.BILLING_ENABLED || options.expirationMode === "never") return;
const date = options.expirationDate && new Date(options.expirationDate);
const maximum = new Date();
maximum.setFullYear(maximum.getFullYear() + 1);
maximum.setHours(23, 59, 59, 999);
const unchanged = !!date && !!current && date.getTime() === new Date(current).getTime();
if (!["remove", "redirect"].includes(options.expirationMode || "") || !date ||
!Number.isFinite(date.getTime()) || date > maximum) throw billingError("invalid_billing_expiration");
!Number.isFinite(date.getTime()) || (date > maximum && !unchanged)) throw billingError("invalid_billing_expiration");
}
+2
View File
@@ -3,11 +3,13 @@ import config from "../../config";
import { BillingEvent, BillingAccount } from "./models";
import { processBillingInbox, billingHealth, billingFailure } from "./events";
import { reconcileBillingAccounts } from "./reconciliation";
import { rememberConfiguredPrices } from "./stripe";
import { createLogger } from "../logger";
const logger = createLogger("billing");
export async function startBillingMaintenance() {
if (!config.BILLING_ENABLED) return;
await Promise.all([BillingEvent.createIndexes(), BillingAccount.createIndexes()]);
await rememberConfiguredPrices();
let eventsRunning = false;
let reconcileRunning = false;
const events = async () => {
+1 -1
View File
@@ -10,7 +10,7 @@ export const BillingAccount = model("billingAccount", new Schema({
export const BillingGrant = model("billingGrant", new Schema({
_id: String, owner: { type: String, index: true }, scope: { type: String, index: true },
planId: String, customerId: String, subscriptionId: String, paymentIntentId: { type: String, index: true },
status: String, until: Date, cancelAtPeriodEnd: Boolean,
status: String, until: Date, cancelAtPeriodEnd: Boolean, purchasedAt: Date,
priceId: String,
revision: { type: Number, default: 0 }, revoked: { type: Boolean, default: false },
revocationReason: String, revokedAt: Date,
+9 -4
View File
@@ -3,7 +3,7 @@ import { randomUUID } from "crypto";
import ConferenceModel from "../model/conference/conferences.model";
import config from "../../config";
import { BillingGrant, BillingQuota } from "./models";
import { billingError, billingPlans } from "./config";
import { billingError, billingPlans, MANUAL_REFRESH_PLANS, isConferencePlan } from "./config";
import RepositoryModel from "../model/anonymizedRepositories/anonymizedRepositories.model";
export function usableGrant(grant: { status?: string | null; until?: Date | null; revoked?: boolean | null }, now = new Date()) {
@@ -14,8 +14,13 @@ export async function allowance(owner: string, repoId?: string, conference?: str
const grants = await BillingGrant.find({ scope: { $in: scopes }, until: { $gt: new Date() }, revoked: false }).lean();
// A conference pays before the author. There is no double charging of allowance.
for (const scope of scopes.reverse()) {
const grant = grants.find(g => g.scope === scope && usableGrant(g));
const plan = grant && billingPlans().find(p => p.id === grant.planId);
// Expiration add-ons share the repository scope but carry no storage allowance.
// Repository add-ons only fund the owner who bought them; the ID could
// otherwise be bought before another account creates that repository.
const plan = grants.filter(g => g.scope === scope && usableGrant(g) && (!scope.startsWith("repo:") || g.owner === owner))
.map(g => billingPlans().find(p => p.id === g.planId && p.capacity)).filter(Boolean)
// Prefer a plan with automatic updates when several fund the same scope.
.sort((a, b) => Number(MANUAL_REFRESH_PLANS.includes(a!.id)) - Number(MANUAL_REFRESH_PLANS.includes(b!.id)))[0];
if (plan) return { scope, repositories: plan.repositories, storage: plan.storage, plan: plan.id };
}
return { scope: `user:${owner}`, repositories: config.FREE_AUTHOR_REPOSITORIES, storage: config.FREE_AUTHOR_STORAGE, plan: "free" };
@@ -74,7 +79,7 @@ export async function assertConferenceAdmission(owner: string, repoId: string, c
if (!conference || !config.BILLING_ENABLED) return;
const conf = await ConferenceModel.findOne({ conferenceID: conference }).lean();
if (!conf || conf.status !== "ready" || conf.startDate > new Date() || conf.endDate <= new Date()) throw billingError("conf_not_activated");
if (conf.plan.planID === "conference" && (await allowance(owner, repoId, conference)).scope !== `conference:${conference}`) throw billingError("conference_payment_required", 402);
if (isConferencePlan(conf.plan.planID) && (await allowance(owner, repoId, conference)).scope !== `conference:${conference}`) throw billingError("conference_payment_required", 402);
}
// Source trees report LFS pointer sizes. Charge resolved bytes before allowing
+76 -28
View File
@@ -1,8 +1,10 @@
import Stripe = require("stripe");
import { randomUUID } from "crypto";
import config from "../../config";
import { BillingAccount, BillingGrant, BillingLock, BillingRevocation, BillingPrice } from "./models";
import { billingError, billingOrigin, billingPlans, requireBilling } from "./config";
import { BillingAccount, BillingGrant, BillingLock, BillingRevocation, BillingPrice, BillingQuota } from "./models";
import { REPOSITORY_ID_COLLATION } from "../repository-id";
import { billingError, billingOrigin, billingPlans, legacyPricePlans, requireBilling, REPOSITORY_ADDONS, REPO_EXTEND_MS, REPO_NEVER_UNTIL, PACK_PLANS, isConferencePlan } from "./config";
import { usableGrant } from "./quotas";
import ConferenceModel from "../model/conference/conferences.model";
import RepositoryModel from "../model/anonymizedRepositories/anonymizedRepositories.model";
@@ -19,6 +21,22 @@ export async function withBillingLock<T>(id: string, work: () => Promise<T>): Pr
try { return await work(); }
finally { clearInterval(heartbeat); await BillingLock.deleteOne({ _id: id, token }); }
}
// The plan a Stripe price pays for: the configured price, then a declared
// legacy price, then a price recorded when it was configured or sold.
export async function planForPrice(priceId: string | undefined, recurring: boolean) {
if (!priceId) return undefined;
const plans = billingPlans().filter(p => p.recurring === recurring);
const configured = plans.find(p => p.priceId === priceId);
if (configured) return configured;
const planId = legacyPricePlans().get(priceId) || (await BillingPrice.findById(priceId).lean())?.planId;
return plans.find(p => p.id === planId);
}
// Record every configured price before it can be replaced, so a later
// configuration change still recognizes subscriptions sold at that price.
export async function rememberConfiguredPrices() {
const prices = [...billingPlans().filter(p => p.priceId).map(p => [p.priceId, p.id] as const), ...legacyPricePlans()];
for (const [priceId, planId] of prices) await BillingPrice.updateOne({ _id: priceId }, { $setOnInsert: { planId } }, { upsert: true });
}
export async function syncSubscription(id: string, stripe = stripeClient()) {
// Compare-and-swap prevents a slow, earlier fetch from overwriting a newer one.
for (let retry = 0; retry < 4; retry++) {
@@ -29,7 +47,7 @@ export async function syncSubscription(id: string, stripe = stripeClient()) {
const account = await BillingAccount.findById(owner).lean();
if (!account || account.customerId !== objectId(sub.customer)) throw billingError("billing_customer_mismatch");
const priceId = sub.items.data[0]?.price.id;
const plan = billingPlans().find(p => p.recurring && p.priceId === priceId);
const plan = await planForPrice(priceId, true);
const invoice = sub.latest_invoice as Stripe.Invoice | null;
const paid = !!plan && sub.items.data.length === 1 && sub.status === "active" && invoice?.status === "paid";
const periodEnd = sub.items.data[0]?.current_period_end;
@@ -45,9 +63,9 @@ export async function syncSubscription(id: string, stripe = stripeClient()) {
}
}
}
const values = { owner, scope: `user:${owner}`, planId: plan?.id || "unknown", customerId: account.customerId,
const values = { owner, scope: `user:${owner}`, planId: plan?.id || "unknown", customerId: account.customerId, priceId,
subscriptionId: id, status: sub.status, cancelAtPeriodEnd: sub.cancel_at_period_end || !!sub.cancel_at,
until: paidThrough,
until: paidThrough, ...(sub.start_date ? { purchasedAt: new Date(sub.start_date * 1000) } : {}),
};
if (!previous) {
try { await BillingGrant.create({ _id: id, ...values }); return; }
@@ -71,33 +89,57 @@ export async function fulfillCheckout(id: string, stripe = stripeClient()) {
const priceId = session.line_items?.data[0]?.price?.id;
const previousGrant = await BillingGrant.findById(id).lean();
const historicalPrice = priceId ? await BillingPrice.findById(priceId).lean() : null;
const previousScope = planId === "conference" ? `conference:${target}` : `repo:${target}`;
const previousScope = isConferencePlan(planId) ? `conference:${target}` : `repo:${target}`;
const acceptedGrant = previousGrant?.owner === owner && previousGrant?.customerId === account.customerId
&& previousGrant?.planId === planId && previousGrant?.scope === previousScope
&& (!previousGrant.priceId || previousGrant.priceId === priceId);
if (!plan || !priceId || session.line_items?.data.length !== 1 || session.line_items.data[0].quantity !== 1 ||
!(priceId === plan.priceId || historicalPrice?.planId === planId || acceptedGrant)) throw billingError("billing_invalid_purchase");
!(priceId === plan.priceId || historicalPrice?.planId === planId || legacyPricePlans().get(priceId) === planId || acceptedGrant)) throw billingError("billing_invalid_purchase");
await BillingPrice.updateOne({ _id: priceId }, { $setOnInsert: { planId } }, { upsert: true });
if (session.payment_status !== "paid" || session.status !== "complete") return;
const intent = session.payment_intent as Stripe.PaymentIntent | null;
const charge = intent?.latest_charge as Stripe.Charge | null;
const revoked = !!charge && (charge.refunded || charge.disputed);
const scope = plan.id === "conference" ? `conference:${target}` : `repo:${target}`;
let until = new Date(session.created * 1000 + 180 * 86400_000);
if (plan.id === "conference") {
const scope = isConferencePlan(plan.id) ? `conference:${target}` : `repo:${target}`;
const purchased = new Date(session.created * 1000);
// A paper pack is fulfilled as its three add-ons. They share the payment
// intent, so a refund or dispute revokes all of them together.
const parts: { _id: string; planId: string }[] = plan.id === "repo_pack"
? PACK_PLANS.map(part => ({ _id: `${id}:${part}`, planId: part })) : [{ _id: id, planId: plan.id }];
const untilFor = async (part: { _id: string; planId: string }) => {
if (part.planId === "repo_never" || part.planId === "repo_slot") return REPO_NEVER_UNTIL;
if (part.planId === "repo_extend") {
// Extensions stack: each purchase adds a year after any earlier paid limit.
// $setOnInsert below keeps this value stable across duplicate deliveries.
const earlier = await BillingGrant.find({ _id: { $ne: part._id }, scope, owner, planId: "repo_extend" }).lean();
const base = Math.max(purchased.getTime(), ...earlier.filter(g => usableGrant(g, purchased)).map(g => g.until!.getTime()));
return new Date(base + REPO_EXTEND_MS);
}
return new Date(purchased.getTime() + 180 * 86400_000);
};
let conferenceEnd: Date | undefined;
if (isConferencePlan(plan.id)) {
const conference = await ConferenceModel.findOne({ conferenceID: target });
// Historical sessions can outlive a deliberately deleted conference.
if (!conference) return;
until = conference.endDate;
conferenceEnd = conference.endDate;
}
await BillingGrant.updateOne({ _id: id }, { $setOnInsert: { owner, scope, planId, customerId: account.customerId,
paymentIntentId: objectId(session.payment_intent), priceId, status: "paid", until }, ...(revoked ? { $set: { revoked: true } } : {}) }, { upsert: true });
const reversal = intent && await BillingRevocation.findById(intent.id).lean();
if (reversal?.reviewed) await BillingGrant.updateOne({ _id: id }, { $set: { revoked: false } });
else if (reversal) await BillingGrant.updateOne({ _id: id }, { $set: { revoked: true, revocationReason: reversal.reason } });
if (plan.id === "conference" && (!revoked || reversal?.reviewed)) {
for (const part of parts) {
const until = conferenceEnd || await untilFor(part);
await BillingGrant.updateOne({ _id: part._id }, { $setOnInsert: { owner, scope, planId: part.planId, customerId: account.customerId,
paymentIntentId: objectId(session.payment_intent), priceId, status: "paid", until, purchasedAt: purchased }, ...(revoked ? { $set: { revoked: true } } : {}) }, { upsert: true });
if (reversal?.reviewed) await BillingGrant.updateOne({ _id: part._id }, { $set: { revoked: false } });
else if (reversal) await BillingGrant.updateOne({ _id: part._id }, { $set: { revoked: true, revocationReason: reversal.reason } });
if (part.planId === "repo_slot" && !(await BillingGrant.findById(part._id).lean())?.revoked) {
// A bought repository no longer counts toward its owner's free allowance.
// Its next admission reserves it in its own repository ledger instead.
await BillingQuota.updateOne({ _id: `user:${owner}` }, { $pull: { allocations: { repoId: target } } });
}
}
if (isConferencePlan(plan.id) && (!revoked || reversal?.reviewed)) {
const grant = await BillingGrant.findById(id).lean();
if (!grant?.revoked) await ConferenceModel.updateOne({ conferenceID: target, status: "pending_payment", "plan.planID": "conference" }, { $set: { status: "ready" } });
if (!grant?.revoked) await ConferenceModel.updateOne({ conferenceID: target, status: "pending_payment", "plan.planID": plan.id }, { $set: { status: "ready" } });
}
}
export async function processBillingEvent(event: Stripe.Event, stripe = stripeClient()) {
@@ -145,15 +187,20 @@ export async function createCheckout(owner: string, planId: string, target = "",
if (!config.BILLING_CHECKOUT_ENABLED) throw billingError("billing_checkout_disabled", 503);
const plan = billingPlans().find(p => p.id === planId && p.priceId);
if (!plan) throw billingError("billing_plan_unavailable");
if (plan.id === "conference") {
if (isConferencePlan(plan.id)) {
// A conference pays for the tier it was created with.
const conf = await ConferenceModel.findOne({ conferenceID: target, owners: owner });
if (!conf || conf.plan.planID !== "conference" || conf.status !== "pending_payment") throw billingError("billing_invalid_conference");
if (!conf || conf.plan.planID !== plan.id || conf.status !== "pending_payment") throw billingError("billing_invalid_conference");
if (conf.endDate <= new Date() || conf.endDate.getTime() - conf.startDate.getTime() > 180 * 86400_000) throw billingError("billing_invalid_review_window");
} else if (plan.id === "author_pass") {
const repo = await RepositoryModel.exists({ repoId: target, owner, status: { $nin: ["removed", "expired"] } });
if (!repo) throw billingError("repo_not_found", 404);
} else if (REPOSITORY_ADDONS.includes(plan.id)) {
// An add-on can be bought for a repository the owner is about to create:
// a full free allowance blocks creation until it is paid for.
if (typeof target !== "string" || !/^[0-9a-zA-Z_-]{3,200}$/.test(target)) throw billingError("repo_not_found", 404);
const repo = await RepositoryModel.findOne({ repoId: target }, null, { collation: REPOSITORY_ID_COLLATION }).select("repoId owner status").lean();
if (repo && (String(repo.owner) !== owner || repo.status === "removed")) throw billingError("repo_not_found", 404);
if (repo?.repoId) target = repo.repoId;
} else target = "";
const destination = returnPath || (plan.id === "conference" ? `/conference/${encodeURIComponent(target)}` : "/profile");
const destination = returnPath || (isConferencePlan(plan.id) ? `/conference/${encodeURIComponent(target)}` : "/profile");
if (returnPath && !/^\/anonymize(?:\/[A-Za-z0-9_-]{1,200})?\?billingDraft=[a-f0-9-]{36}$/.test(returnPath)) throw billingError("billing_invalid_draft");
const outcomeUrl = (outcome: string) => `${billingOrigin()}${destination}${destination.includes("?") ? "&" : "?"}billing=${outcome}`;
return withBillingLock(`checkout:${owner}`, async () => {
@@ -211,7 +258,9 @@ export async function createCheckout(owner: string, planId: string, target = "",
if (subscriptions.data.some(s => !["canceled", "incomplete_expired"].includes(s.status))) throw billingError("billing_subscription_exists", 409);
after = subscriptions.has_more ? subscriptions.data.at(-1)?.id : undefined;
} while (after);
} else if (await BillingGrant.exists({ scope: `${plan.id === "conference" ? "conference" : "repo"}:${target}`, revoked: false, until: { $gt: new Date() } })) throw billingError("billing_already_paid", 409);
} else if (isConferencePlan(plan.id) ? await BillingGrant.exists({ scope: `conference:${target}`, revoked: false, until: { $gt: new Date() } })
// Extensions stack, so only live updates and never expire can already be paid.
: !["repo_extend", "repo_pack"].includes(plan.id) && await BillingGrant.exists({ scope: `repo:${target}`, owner, planId: plan.id, revoked: false, until: { $gt: new Date() } })) throw billingError("billing_already_paid", 409);
// Persist the idempotency key before sending a request. A timeout can safely retry it.
if (!account.checkout?.key || account.checkout.id) {
account.checkout = { key: randomUUID(), planId, target, createdAt: new Date(), expiresAt: new Date(Date.now() + 1860_000) };
@@ -232,10 +281,9 @@ export async function createCheckout(owner: string, planId: string, target = "",
account.checkout.params = params;
await account.save();
}
if (!plan.recurring) {
const acceptedPrice = (account.checkout.params as Stripe.Checkout.SessionCreateParams).line_items?.[0]?.price;
if (acceptedPrice) await BillingPrice.updateOne({ _id: acceptedPrice }, { $setOnInsert: { planId } }, { upsert: true });
}
// Subscriptions too: their price must stay recognizable after a price change.
const acceptedPrice = (account.checkout.params as Stripe.Checkout.SessionCreateParams).line_items?.[0]?.price;
if (acceptedPrice) await BillingPrice.updateOne({ _id: acceptedPrice }, { $setOnInsert: { planId } }, { upsert: true });
const session = await stripe.checkout.sessions.create(account.checkout.params as Stripe.Checkout.SessionCreateParams, { idempotencyKey: account.checkout.key });
account.checkout.id = session.id;
await account.save();
+48 -5
View File
@@ -1,13 +1,14 @@
import * as express from "express";
import { randomBytes } from "crypto";
import { createHash, randomBytes } from "crypto";
import { ensureAuthenticated } from "./connection";
import { getUser, handleError } from "./route-utils";
import config from "../../config";
import { BillingAccount, BillingGrant } from "../../core/billing/models";
import { billingOrigin, billingPlans, requireBilling } from "../../core/billing/config";
import { billingOrigin, billingPlans, requireBilling, PACK_PLANS, REPO_NEVER_UNTIL } from "../../core/billing/config";
import { createCheckout, stripeClient, fulfillCheckout, syncSubscription } from "../../core/billing/stripe";
import { saveBillingDraft, getBillingDraft } from "../../core/billing/drafts";
import { allowance } from "../../core/billing/quotas";
import { allowance, usableGrant } from "../../core/billing/quotas";
import RepositoryModel from "../../core/model/anonymizedRepositories/anonymizedRepositories.model";
import { receiveBillingEvent, billingFailure } from "../../core/billing/events";
import { createLogger } from "../../core/logger";
@@ -34,6 +35,44 @@ async function displayedPrice(id: string) {
priceCache.set(id, { expires: Date.now() + 60_000, value });
return value;
}
type GrantRow = { _id?: unknown; planId?: string | null; scope?: string | null; status?: string | null; until?: Date | null;
cancelAtPeriodEnd?: boolean | null; revoked?: boolean | null; revocationReason?: string | null; purchasedAt?: Date | null; priceId?: string | null };
// A readable purchase list: pack parts grouped, internal IDs and the
// year-9999 "no end" sentinel hidden, statuses reduced to a few states.
export async function purchaseHistory(grants: GrantRow[], now = new Date()) {
const state = (g: GrantRow) => g.revoked ? (g.revocationReason === "dispute" ? "disputed" : "refunded")
: g.status === "past_due" ? "overdue" : usableGrant(g, now) ? "active" : "ended";
const until = (g: GrantRow) => g.until && g.until.getTime() < REPO_NEVER_UNTIL.getTime() ? g.until : null;
const groups = new Map<string, GrantRow[]>();
for (const grant of grants) {
const id = String(grant._id);
const key = /^cs_.+:(repo_slot|author_pass|repo_extend)$/.test(id) && grants.filter(g => String(g._id).startsWith(id.split(":")[0] + ":")).length === PACK_PLANS.length
? id.split(":")[0] : id;
groups.set(key, [...(groups.get(key) || []), grant]);
}
const purchases = [];
for (const [key, rows] of groups) {
const first = rows[0];
const pack = rows.length > 1;
let amount: number | null = null, currency: string | null = null, interval: string | null = null;
if (first.priceId) {
try { const price = await displayedPrice(first.priceId); amount = price.unit_amount; currency = price.currency; interval = price.recurring?.interval || null; } catch { /* Price display is optional. */ }
}
const [kind, target] = String(first.scope || "").split(/:(.*)/s);
const states = rows.map(state);
purchases.push({
id: createHash("sha256").update(key).digest("hex").slice(0, 12),
planId: pack ? "repo_pack" : first.planId, recurring: ["author_monthly", "author_annual"].includes(first.planId || ""),
repoId: kind === "repo" ? target : null, conference: kind === "conference" ? target : null,
status: states.includes("disputed") ? "disputed" : states.includes("refunded") ? "refunded" : states.includes("overdue") ? "overdue"
: states.includes("active") ? "active" : "ended",
until: pack ? null : until(first), purchasedAt: first.purchasedAt || null, cancelAtPeriodEnd: !!first.cancelAtPeriodEnd, amount, currency, interval,
parts: pack ? rows.map(g => ({ planId: g.planId, until: until(g), status: state(g) })) : [],
});
}
return purchases.sort((a, b) => Number(b.status === "active" || b.status === "overdue") - Number(a.status === "active" || a.status === "overdue")
|| (b.purchasedAt?.getTime() || 0) - (a.purchasedAt?.getTime() || 0));
}
const router = express.Router();
router.use(ensureAuthenticated);
router.get("/", async (req, res) => {
@@ -42,14 +81,18 @@ router.get("/", async (req, res) => {
req.session.billingCSRF ||= randomBytes(32).toString("hex");
await new Promise<void>((resolve, reject) => req.session.save(error => error ? reject(error) : resolve()));
const account = await BillingAccount.findById(user.id).lean();
const grants = await BillingGrant.find({ owner: user.id }).select("planId scope status until cancelAtPeriodEnd revoked").lean();
const grants = await BillingGrant.find({ owner: user.id }).select("planId scope status until cancelAtPeriodEnd revoked revocationReason purchasedAt priceId").lean();
const repositories = config.BILLING_ENABLED ? await RepositoryModel.find({ owner: user.id, status: { $ne: "removed" } })
.select("repoId status options.expirationMode options.expirationDate").sort({ anonymizeDate: -1 }).limit(500).lean() : [];
const plans = config.BILLING_ENABLED ? await Promise.all(billingPlans().filter(p => p.priceId).map(async p => {
const price = await displayedPrice(p.priceId);
return { id: p.id, name: p.name, description: p.description, repositories: p.repositories, storage: p.storage,
amount: price.unit_amount, currency: price.currency, interval: price.recurring?.interval, intervalCount: price.recurring?.interval_count };
})) : [];
res.set("Cache-Control", "no-store").json({ enabled: config.BILLING_ENABLED, csrf: req.session.billingCSRF, plans, checkoutEnabled: config.BILLING_CHECKOUT_ENABLED,
res.set("Cache-Control", "no-store").json({ enabled: config.BILLING_ENABLED, csrf: req.session.billingCSRF, plans, checkoutEnabled: config.BILLING_CHECKOUT_ENABLED, freeExpirationMonths: config.FREE_EXPIRATION_MONTHS,
grants: grants.map(g => ({ planId: g.planId, scope: g.scope, status: g.revoked ? "revoked" : g.status, until: g.until, cancelAtPeriodEnd: g.cancelAtPeriodEnd })),
purchases: config.BILLING_ENABLED ? await purchaseHistory(grants) : [],
repositories: repositories.map(r => ({ repoId: r.repoId, status: r.status, expirationMode: r.options?.expirationMode, expirationDate: r.options?.expirationDate })),
portal: !!account?.customerId, allowance: config.BILLING_ENABLED ? await allowance(user.id) : null });
} catch (error) { handleError(error, res, req); }
});
+40 -24
View File
@@ -1,5 +1,7 @@
import config from "../../config";
import { billingError } from "../../core/billing/config";
import { billingError, billingPlans, isConferencePlan } from "../../core/billing/config";
import { BillingGrant } from "../../core/billing/models";
import { usableGrant } from "../../core/billing/quotas";
import * as express from "express";
import AnonymousError from "../../core/AnonymousError";
import Conference from "../../core/Conference";
@@ -13,15 +15,26 @@ const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
const plans = [
{ id: "free_conference", name: "Free", pricePerRepo: 0, storagePerRepo: -1,
description: "<li>Uses each author's allowance</li><li>Conference dashboard</li>" },
{ id: "conference", name: "Review package", pricePerRepo: 0, storagePerRepo: config.PAID_CONFERENCE_STORAGE,
description: `<li>Up to ${config.PAID_CONFERENCE_REPOSITORIES} repositories</li><li>Up to 180 days</li><li>Prepaid at checkout</li>` },
];
// The free plan plus one review package per venue size, smallest first.
function conferencePlans() {
return [
{ id: "free_conference", name: "Free", pricePerRepo: 0, storagePerRepo: -1, repositories: 0, priceId: "",
description: "<li>Uses each author's allowance</li><li>Conference dashboard</li>" },
...billingPlans().filter(p => isConferencePlan(p.id)).map(p => ({ id: p.id as string, name: p.name, pricePerRepo: 0,
storagePerRepo: p.storage, repositories: p.repositories, priceId: p.priceId,
description: `<li>Up to ${p.repositories} repositories</li><li>Up to 180 days</li><li>Prepaid at checkout</li>` })),
];
}
// With billing enabled every new conference is a paid review package; the free
// plan is only offered on deployments without billing.
export function offeredConferencePlans() {
if (!config.BILLING_ENABLED) return conferencePlans().filter(p => p.id === "free_conference").map(({ priceId: _, ...p }) => p);
return conferencePlans().filter(p => isConferencePlan(p.id) && p.priceId).map(({ priceId: _, ...p }) => p);
}
router.get("/plans", async (req: express.Request, res: express.Response) => {
res.json(config.BILLING_ENABLED && config.STRIPE_CONFERENCE_PRICE ? plans : plans.filter(p => p.id === "free_conference"));
res.json(offeredConferencePlans());
});
router.get("/", async (req: express.Request, res: express.Response) => {
@@ -78,14 +91,16 @@ function validateConferenceForm(conf: any, isNew: boolean) {
object: conf,
httpStatus: 400,
});
if (!conf.plan || plans.filter((p) => p.id == conf.plan.planID).length != 1)
if (!conf.plan || conferencePlans().filter((p) => p.id == conf.plan.planID).length != 1)
throw new AnonymousError("invalid_plan", {
object: conf,
httpStatus: 400,
});
if (!Number.isFinite(new Date(conf.startDate).getTime()) || !Number.isFinite(new Date(conf.endDate).getTime())) throw billingError("conf_start_date_invalid");
if (conf.plan.planID === "conference" && ((isNew && (!config.BILLING_ENABLED || !config.STRIPE_CONFERENCE_PRICE)) || new Date(conf.endDate).getTime() - new Date(conf.startDate).getTime() > 180 * 86400_000)) throw billingError("billing_invalid_review_window");
const plan = plans.filter((p) => p.id == conf.plan.planID)[0];
if (isConferencePlan(conf.plan.planID) && ((isNew && (!config.BILLING_ENABLED || !offeredConferencePlans().some(p => p.id === conf.plan.planID))) || new Date(conf.endDate).getTime() - new Date(conf.startDate).getTime() > 180 * 86400_000)) throw billingError("billing_invalid_review_window");
// A new conference can only use a plan that is offered: paid when billing is on.
if (isNew && !offeredConferencePlans().some(p => p.id === conf.plan.planID)) throw new AnonymousError("invalid_plan", { object: conf, httpStatus: 400 });
const plan = conferencePlans().filter((p) => p.id == conf.plan.planID)[0];
if (plan.pricePerRepo > 0) {
const billing = conf.billing;
if (!billing)
@@ -135,7 +150,7 @@ export function applyConferenceForm(
model.name = body.name;
model.startDate = new Date(body.startDate);
model.endDate = new Date(body.endDate);
if (isNew) model.status = body.plan?.planID === "conference" ? "pending_payment" : "ready";
if (isNew) model.status = isConferencePlan(body.plan?.planID) ? "pending_payment" : "ready";
model.url = body.url;
model.options = body.options;
if (isNew) model.repositories = [];
@@ -161,10 +176,10 @@ router.post(
}
if (req.params.conferenceID) {
if (req.body.plan?.planID !== model.plan.planID) throw billingError("billing_plan_change_not_supported");
if (model.plan.planID === "conference" && (new Date(req.body.startDate).getTime() !== model.startDate.getTime() || new Date(req.body.endDate).getTime() !== model.endDate.getTime())) throw billingError("billing_review_window_locked");
if (isConferencePlan(model.plan.planID) && (new Date(req.body.startDate).getTime() !== model.startDate.getTime() || new Date(req.body.endDate).getTime() !== model.endDate.getTime())) throw billingError("billing_review_window_locked");
}
// Historical conference plans remain editable without being offered for purchase.
const legacyPlan = req.params.conferenceID && !plans.some(p => p.id === model.plan.planID);
const legacyPlan = req.params.conferenceID && !offeredConferencePlans().some(p => p.id === model.plan.planID);
validateConferenceForm(legacyPlan ? { ...req.body, plan: { planID: "free_conference" } } : req.body, !req.params.conferenceID);
applyConferenceForm(model, req.body, !req.params.conferenceID);
@@ -172,17 +187,11 @@ router.post(
model.owners.push(user.model.id);
model.conferenceID = req.body.conferenceID;
const chosen = conferencePlans().find((p) => p.id == req.body.plan.planID)!;
model.plan = {
planID: req.body.plan.planID,
pricePerRepository: plans.filter(
(p) => p.id == req.body.plan.planID
)[0].pricePerRepo,
quota: {
size: plans.filter((p) => p.id == req.body.plan.planID)[0]
.storagePerRepo,
file: 0,
repository: req.body.plan.planID === "conference" ? config.PAID_CONFERENCE_REPOSITORIES : 0,
},
planID: chosen.id,
pricePerRepository: chosen.pricePerRepo,
quota: { size: chosen.storagePerRepo, file: 0, repository: chosen.repositories },
};
if (req.body.billing)
@@ -232,6 +241,11 @@ router.get(
});
const user = await getUser(req);
const conference = new Conference(data);
// Authors see whether the conference pays for their repository's hosting.
const now = new Date();
// Mirror admission: a refunded or disputed package no longer hosts anything.
const hosting = config.BILLING_ENABLED && isConferencePlan(data.plan?.planID) && data.status === "ready" && data.startDate <= now && data.endDate > now
&& (await BillingGrant.find({ scope: `conference:${data.conferenceID}`, revoked: false, until: { $gt: now } }).select("status until revoked").lean()).some(g => usableGrant(g, now));
try {
isOwnerOrAdmin(conference.ownerIDs, user);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -239,6 +253,7 @@ router.get(
o.repositories = (await conference.repositories()).map((r) =>
r.toJSON()
);
o.hosting = hosting;
res.json(o);
} catch {
return res.json({
@@ -248,6 +263,7 @@ router.get(
startDate: conference.startDate,
endDate: conference.endDate,
options: conference.options,
hosting,
});
}
} catch (error) {
+15 -7
View File
@@ -1,6 +1,6 @@
import Repository from "../../core/Repository";
import { sourceStorageSize } from "../../core/source/Source";
import { neverExpirationDeadline, validateBillingExpiration } from "../../core/billing/expiration";
import { maximumExpiration, neverExpirationDeadline, validateBillingExpiration } from "../../core/billing/expiration";
import config from "../../config";
import { assertConferenceAdmission, reserveRepository } from "../../core/billing/quotas";
import { resetOwnerAccessAlerts } from "../../core/owner-notifications";
@@ -206,10 +206,17 @@ router.post(
const user = await getUser(req);
isOwnerCoauthorOrAdmin(repo, user);
const newExpiration = extendExpirationDate(
const maximum = await maximumExpiration(String(repo.model.owner), repo.repoId, repo.model.conference);
let newExpiration = extendExpirationDate(
repo.model.options.expirationDate
);
validateBillingExpiration({ ...repo.model.options, expirationDate: newExpiration });
// Extend as far as the repository's limit allows, never shortening it.
if (config.BILLING_ENABLED && newExpiration > maximum) {
const current = repo.model.options.expirationDate && new Date(repo.model.options.expirationDate);
if (current && current >= maximum) throw new AnonymousError("expiration_limit_reached", { httpStatus: 402 });
newExpiration = maximum;
}
validateBillingExpiration({ ...repo.model.options, expirationDate: newExpiration }, maximum);
const reactivating = repo.status === RepositoryStatus.EXPIRED;
if (reactivating) await assertConferenceAdmission(String(repo.model.owner), repo.repoId, repo.model.conference);
const updates: Record<string, Date | string | null> = {
@@ -571,11 +578,12 @@ router.post(
// Keep proposed settings separate until cleanup passes the persisted fences.
const proposed = new AnonymizedRepositoryModel(repo.model.toObject());
if (repo.model.billingManaged) validateBillingExpiration(repoUpdate.options);
if (repo.model.billingManaged) validateBillingExpiration(repoUpdate.options,
await maximumExpiration(String(repo.model.owner), repo.repoId, repoUpdate.conference), repo.model.options.expirationDate);
if (config.BILLING_ENABLED && repoUpdate.options.expirationMode === "never" &&
(repo.model.options.expirationMode !== "never" || repo.model.billingNeverExpiresUntil)) {
proposed.billingNeverExpiresUntil = await neverExpirationDeadline(String(repo.model.owner),
repo.model.options.expirationMode === "never" ? repo.model.billingNeverExpiresUntil : undefined);
repo.model.options.expirationMode === "never" ? repo.model.billingNeverExpiresUntil : undefined, repo.repoId);
} else if (repoUpdate.options.expirationMode !== "never") {
proposed.billingNeverExpiresUntil = undefined;
}
@@ -801,9 +809,9 @@ router.post("/", async (req, res) => {
repo.owner = user.id;
repo.githubAccess = selectedAccess.binding;
validateBillingExpiration(repoUpdate.options);
validateBillingExpiration(repoUpdate.options, await maximumExpiration(user.id, repoUpdate.repoId, repoUpdate.conference));
if (repoUpdate.options.expirationMode === "never") {
repo.billingNeverExpiresUntil = await neverExpirationDeadline(user.id);
repo.billingNeverExpiresUntil = await neverExpirationDeadline(user.id, undefined, repoUpdate.repoId);
}
updateRepoModel(repo, repoUpdate);
repo.source.type = "GitHubStream";
+199 -2
View File
@@ -25,7 +25,8 @@ async function rejects(promise, message) {
});
beforeEach(async () => {
Object.assign(config, { BILLING_ENABLED: true, FREE_AUTHOR_REPOSITORIES: 3, FREE_AUTHOR_STORAGE: 100,
PAID_AUTHOR_STORAGE: 1000, PAID_AUTHOR_REPOSITORIES: 20, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly", STRIPE_AUTHOR_PASS_PRICE: "price_pass", STRIPE_CONFERENCE_PRICE: "price_conf" });
PAID_AUTHOR_STORAGE: 1000, PAID_AUTHOR_REPOSITORIES: 20, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly", STRIPE_AUTHOR_PASS_PRICE: "price_pass", STRIPE_CONFERENCE_PRICE: "price_conf", STRIPE_CONFERENCE_SMALL_PRICE: "price_conf_small", STRIPE_CONFERENCE_LARGE_PRICE: "price_conf_large", PAID_CONFERENCE_SMALL_REPOSITORIES: 25, STRIPE_REPO_EXTEND_PRICE: "", STRIPE_REPO_NEVER_PRICE: "", STRIPE_REPO_SLOT_PRICE: "", STRIPE_REPO_PACK_PRICE: "" });
Object.assign(config, { STRIPE_AUTHOR_ANNUAL_PRICE: "price_annual", STRIPE_LEGACY_PRICES: "" });
await Promise.all([BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft, Repo, Conference].map(m => m.deleteMany({})));
await BillingAccount.create({ _id: owner, customerId: "cus_owner" });
});
@@ -243,6 +244,108 @@ async function rejects(promise, message) {
customer: "cus_owner", status: "complete", payment_status: status, created: Math.floor(Date.now() / 1000),
line_items: { data: [{ price: { id: "price_pass" }, quantity: 1 }] }, payment_intent: { id: "pi_pass", latest_charge: { amount_refunded: 0, disputed: false } } };
}
function addonSession(id, planId, created = Math.floor(Date.now() / 1000)) {
return { id, metadata: { application: "anonymous-github", owner, planId, target: "repo" },
customer: "cus_owner", status: "complete", payment_status: "paid", created,
line_items: { data: [{ price: { id: `price_${planId}` }, quantity: 1 }] }, payment_intent: { id: `pi_${id}`, latest_charge: { amount_refunded: 0, disputed: false } } };
}
function sessionsStripe(sessions) {
return { checkout: { sessions: { retrieve: async id => sessions.find(s => s.id === id) } } };
}
it("stacks extended expiration purchases and records never expire once", async () => {
Object.assign(config, { STRIPE_REPO_EXTEND_PRICE: "price_repo_extend", STRIPE_REPO_NEVER_PRICE: "price_repo_never" });
const year = 365 * 86400000;
const first = addonSession("cs_extend_1", "repo_extend");
const second = addonSession("cs_extend_2", "repo_extend", first.created + 60);
const never = addonSession("cs_never", "repo_never");
const stripe = sessionsStripe([first, second, never]);
await fulfillCheckout(first.id, stripe);
await fulfillCheckout(second.id, stripe);
// A duplicate delivery must not add another year.
await fulfillCheckout(second.id, stripe);
expect((await BillingGrant.findById(first.id)).until.getTime()).to.equal(first.created * 1000 + year);
expect((await BillingGrant.findById(second.id)).until.getTime()).to.equal(first.created * 1000 + 2 * year);
await fulfillCheckout(never.id, stripe);
expect((await BillingGrant.findById(never.id)).until.toISOString()).to.equal("9999-12-31T00:00:00.000Z");
});
it("hosts an extra repository outside the free allowance for its buyer only", async () => {
config.STRIPE_REPO_SLOT_PRICE = "price_repo_slot";
await Promise.all(["a", "b", "repo"].map(id => reserveRepository(owner, id, 10)));
await rejects(reserveRepository(owner, "four", 10), "quota_exceeded");
// Buying a slot for a counted repository frees its place in the free ledger.
const slot = addonSession("cs_slot", "repo_slot");
await fulfillCheckout(slot.id, sessionsStripe([slot]));
expect((await BillingGrant.findById(slot.id)).until.toISOString()).to.equal("9999-12-31T00:00:00.000Z");
expect((await BillingQuota.findById(`user:${owner}`)).allocations.map(a => a.repoId)).to.not.include("repo");
expect((await allowance(owner, "repo")).plan).to.equal("repo_slot");
await reserveRepository(owner, "repo", 10);
await reserveRepository(owner, "four", 10);
// Another account cannot use a slot bought for the same repository ID.
expect((await allowance("507f1f77bcf86cd799439099", "repo")).plan).to.equal("free");
config.STRIPE_REPO_SLOT_PRICE = "";
});
it("fulfills a paper pack as three add-ons that a refund revokes together", async () => {
config.STRIPE_REPO_PACK_PRICE = "price_repo_pack";
const pack = addonSession("cs_pack", "repo_pack");
const stripe = sessionsStripe([pack]);
await fulfillCheckout(pack.id, stripe);
await fulfillCheckout(pack.id, stripe);
const grants = await BillingGrant.find({ paymentIntentId: "pi_cs_pack" }).lean();
expect(grants.map(g => g.planId).sort()).to.deep.equal(["author_pass", "repo_extend", "repo_slot"]);
expect(grants.find(g => g.planId === "repo_extend").until.getTime()).to.equal(pack.created * 1000 + 365 * 86400000);
// Live updates win over the manual-refresh slot while both fund the repository.
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_cs_pack", refunded: true } } },
{ ...stripe, invoicePayments: { list: async () => ({ data: [] }) } });
expect(await BillingGrant.countDocuments({ paymentIntentId: "pi_cs_pack", revoked: true })).to.equal(3);
expect((await allowance(owner, "repo")).plan).to.equal("free");
config.STRIPE_REPO_PACK_PRICE = "";
});
it("does not let expiration add-ons replace the storage allowance", async () => {
await BillingGrant.create([{ _id: "extend", owner, scope: "repo:repo", status: "paid", until: future(), planId: "repo_extend" },
{ _id: "never", owner, scope: "repo:repo", status: "paid", until: future(), planId: "repo_never" }]);
expect((await allowance(owner, "repo")).plan).to.equal("free");
await BillingGrant.create({ _id: "pass", owner, scope: "repo:repo", status: "paid", until: future(), planId: "author_pass" });
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
});
it("limits dated expiration by free window, add-on, subscription and conference", async () => {
const { maximumExpiration } = require("../src/core/billing/expiration");
const now = new Date("2026-01-10T12:00:00Z");
const months = (date, n) => { const d = new Date(date); d.setMonth(d.getMonth() + n); d.setHours(23, 59, 59, 999); return d.getTime(); };
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 5));
const extended = new Date("2027-06-01T00:00:00Z");
await BillingGrant.create({ _id: "extend", owner, scope: "repo:repo", status: "paid", until: extended, planId: "repo_extend" });
const withAddon = await maximumExpiration(owner, "repo", undefined, now);
expect(withAddon.getTime()).to.be.greaterThan(extended.getTime() - 86400000);
expect((await maximumExpiration(owner, "other", undefined, now)).getTime()).to.equal(months(now, 5));
await BillingGrant.updateOne({ _id: "extend" }, { $set: { revoked: true } });
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 5));
await Conference.create({ conferenceID: "LONG", owners: [owner], status: "ready", startDate: now, endDate: new Date("2026-09-01T00:00:00Z"), plan: { planID: "conference" } });
expect((await maximumExpiration(owner, "repo", "LONG", now)).toISOString()).to.equal("2026-09-01T00:00:00.000Z");
await BillingGrant.create({ _id: "sub", owner, scope: `user:${owner}`, status: "active", until: new Date("2026-02-10T00:00:00Z"), planId: "author_monthly" });
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 12));
});
it("lets a Never expire add-on keep one repository online until it is revoked", async () => {
const { neverExpirationDeadline, reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
await rejects(neverExpirationDeadline(owner, undefined, "repo"), "subscription_required_for_never_expiration");
await BillingGrant.create({ _id: "never", owner, scope: "repo:repo", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_never" });
const before = Date.now();
const deadline = await neverExpirationDeadline(owner, undefined, "repo");
expect(deadline.getTime()).to.be.at.least(before + NEVER_EXPIRATION_GRACE_MS);
await rejects(neverExpirationDeadline(owner, undefined, "other"), "subscription_required_for_never_expiration");
await Repo.create({ repoId: "repo", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: new Date(Date.now() + 86400000) });
// A valid add-on rolls a close deadline forward to the full grace period.
await reconcileNeverExpiration();
const rolled = (await Repo.findOne({ repoId: "repo" })).billingNeverExpiresUntil.getTime();
expect(rolled).to.be.at.least(before + NEVER_EXPIRATION_GRACE_MS);
// A recent deadline is not rewritten on every run.
await reconcileNeverExpiration(new Date(Date.now() + 86400000));
expect((await Repo.findOne({ repoId: "repo" })).billingNeverExpiresUntil.getTime()).to.equal(rolled);
// After a refund the deadline stops moving and the repository expires.
await BillingGrant.updateOne({ _id: "never" }, { $set: { revoked: true } });
await reconcileNeverExpiration(new Date(rolled), async () => {});
expect((await Repo.findOne({ repoId: "repo" })).options.expirationMode).to.equal("remove");
});
it("ignores an unfinished recurring Checkout before Stripe assigns a subscription", async () => {
const stripe = { checkout: { sessions: { retrieve: async () => ({
id: "cs_open", metadata: { application: "anonymous-github", owner, planId: "author_monthly" },
@@ -350,6 +453,100 @@ async function rejects(promise, message) {
await BillingGrant.updateOne({ _id: "cs_conf" }, { $set: { revoked: true } });
await rejects(assertConferenceAdmission(owner, "repo", "CONF"), "conference_payment_required");
});
it("activates a smaller conference package with its own capacity and only for its tier", async () => {
await Conference.create({ conferenceID: "WS", owners: [owner], status: "pending_payment", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference_workshop" } });
await rejects(createCheckout(owner, "conference_large", "WS", {}), "billing_invalid_conference");
const value = session(); value.id = "cs_ws"; value.metadata.planId = "conference_workshop"; value.metadata.target = "WS"; value.line_items.data[0].price.id = "price_conf_small";
await fulfillCheckout("cs_ws", { checkout: { sessions: { retrieve: async () => value } } });
expect((await Conference.findOne({ conferenceID: "WS" })).status).to.equal("ready");
const limits = await allowance(owner, "repo", "WS");
expect(limits.scope).to.equal("conference:WS");
expect(limits.plan).to.equal("conference_workshop");
expect(limits.repositories).to.equal(25);
await assertConferenceAdmission(owner, "repo", "WS");
});
it("keeps legacy and new annual subscriptions after the annual price is replaced", async () => {
const { rememberConfiguredPrices } = require("../src/core/billing/stripe");
// Deployed before the switch: the old annual price is recorded at startup.
await rememberConfiguredPrices();
const annual = (id, price) => subscription({ id, items: { data: [{ price: { id: price }, current_period_end: Math.floor(future().getTime() / 1000) }] } });
let legacy = annual("sub_owner", "price_annual");
const stripe = { subscriptions: { retrieve: async id => id === "sub_owner" ? legacy : annual(id, "price_annual_new") } };
config.STRIPE_AUTHOR_ANNUAL_PRICE = "price_annual_new";
await syncSubscription("sub_owner", stripe);
let grant = await BillingGrant.findById("sub_owner");
expect(grant.planId).to.equal("author_annual");
expect(grant.priceId).to.equal("price_annual");
expect((await allowance(owner)).plan).to.equal("author_annual");
// Webhook updates (renewal, cancellation) keep resolving the legacy price.
legacy = { ...legacy, cancel_at_period_end: true };
await processBillingEvent({ type: "customer.subscription.updated", data: { object: { id: "sub_owner" } } }, stripe);
grant = await BillingGrant.findById("sub_owner");
expect(grant.planId).to.equal("author_annual");
expect(grant.cancelAtPeriodEnd).to.equal(true);
// New subscriptions use the new price.
await processBillingEvent({ type: "customer.subscription.created", data: { object: { id: "sub_new" } } }, stripe);
expect((await BillingGrant.findById("sub_new")).planId).to.equal("author_annual");
});
it("recognizes a declared legacy price without a recorded one", async () => {
config.STRIPE_AUTHOR_ANNUAL_PRICE = "price_annual_new";
const stripe = { subscriptions: { retrieve: async () => subscription({ items: { data: [{ price: { id: "price_annual_old" }, current_period_end: Math.floor(future().getTime() / 1000) }] } }) } };
await syncSubscription("sub_owner", stripe);
expect((await BillingGrant.findById("sub_owner")).planId).to.equal("unknown");
config.STRIPE_LEGACY_PRICES = "price_annual_old:author_annual";
await syncSubscription("sub_owner", stripe);
expect((await BillingGrant.findById("sub_owner")).planId).to.equal("author_annual");
expect((await allowance(owner)).plan).to.equal("author_annual");
});
it("keeps historical one-time purchases after their price is replaced", async () => {
const value = session(); value.id = "cs_old_pass";
await fulfillCheckout("cs_old_pass", { checkout: { sessions: { retrieve: async () => value } } });
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
config.STRIPE_AUTHOR_PASS_PRICE = "price_pass_new";
// A redelivered webhook for the old sale still fulfills and keeps the grant.
await fulfillCheckout("cs_old_pass", { checkout: { sessions: { retrieve: async () => value } } });
expect((await BillingGrant.findById("cs_old_pass")).revoked).to.not.equal(true);
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
// A sale at an old price never recorded is accepted once it is declared.
const other = session(); other.id = "cs_older"; other.metadata.target = "other"; other.line_items.data[0].price.id = "price_pass_older";
await rejects(fulfillCheckout("cs_older", { checkout: { sessions: { retrieve: async () => other } } }), "billing_invalid_purchase");
config.STRIPE_LEGACY_PRICES = "price_pass_older:author_pass";
await fulfillCheckout("cs_older", { checkout: { sessions: { retrieve: async () => other } } });
expect((await allowance(owner, "other")).plan).to.equal("author_pass");
});
it("keeps a Never expire link online when reconciliation recovers its purchase", async () => {
const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
const now = new Date();
await Repo.create({ repoId: "recovered", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: new Date(now.getTime() - 1000) });
await reconcileNeverExpiration(now, async () => {
await BillingGrant.create({ _id: "never-recovered", owner, scope: "repo:recovered", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_never" });
});
const repo = await Repo.findOne({ repoId: "recovered" });
expect(repo.options.expirationMode).to.equal("never");
expect(repo.billingNeverExpiresUntil.getTime()).to.equal(now.getTime() + NEVER_EXPIRATION_GRACE_MS);
});
it("keeps automatic updates for a paid-slot repository while its owner subscribes", async () => {
const { hasAutomaticUpdates } = require("../src/core/billing/expiration");
await BillingGrant.create({ _id: "slot", owner, scope: "repo:slotted", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_slot" });
expect(await hasAutomaticUpdates(owner, "slotted")).to.equal(false);
await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until: future(), planId: "author_monthly" });
expect(await hasAutomaticUpdates(owner, "slotted")).to.equal(true);
});
it("only reports conference hosting while its package payment stands", async () => {
const utils = require("../src/server/routes/route-utils");
const router = require("../src/server/routes/conference").default;
const get = router.stack.find(layer => layer.route?.path === "/:conferenceID" && layer.route.methods.get).route.stack[0].handle;
const originalUser = utils.getUser;
utils.getUser = async () => ({ model: { id: "someone-else" }, isAdmin: false });
try {
await Conference.create({ conferenceID: "PAID", owners: [owner], status: "ready", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference" } });
await BillingGrant.create({ _id: "conf-paid", owner, scope: "conference:PAID", status: "paid", until: future(), planId: "conference" });
const hosting = async () => { let body; await get({ params: { conferenceID: "PAID" } }, { json: value => { body = value; } }); return body.hosting; };
expect(await hosting()).to.equal(true);
await BillingGrant.updateOne({ _id: "conf-paid" }, { $set: { revoked: true } });
expect(await hosting()).to.equal(false);
} finally { utils.getUser = originalUser; }
});
it("prevents two simultaneous checkout sessions and reuses an open session", async () => {
let creates = 0;
const stripe = { prices: { retrieve: async () => ({ active: true, currency: "eur", unit_amount: 500, recurring: { interval: "month", interval_count: 1 } }) }, subscriptions: { list: async () => ({ data: [] }) }, checkout: { sessions: {
@@ -389,7 +586,7 @@ async function rejects(promise, message) {
const Users = require("../src/core/model/users/users.model").default;
const routes = require("../src/server/routes/billing");
await Users.updateOne({ _id: owner }, { $set: { username: "billing-test-author", status: "active" } }, { upsert: true });
Object.assign(config, { STRIPE_SECRET_KEY: "sk_test_example", STRIPE_WEBHOOK_SECRET: "whsec_test_example", STRIPE_AUTHOR_MONTHLY_PRICE: "", STRIPE_AUTHOR_ANNUAL_PRICE: "", STRIPE_AUTHOR_PASS_PRICE: "", STRIPE_CONFERENCE_PRICE: "" });
Object.assign(config, { STRIPE_SECRET_KEY: "sk_test_example", STRIPE_WEBHOOK_SECRET: "whsec_test_example", STRIPE_AUTHOR_MONTHLY_PRICE: "", STRIPE_AUTHOR_ANNUAL_PRICE: "", STRIPE_AUTHOR_PASS_PRICE: "", STRIPE_CONFERENCE_PRICE: "", STRIPE_CONFERENCE_SMALL_PRICE: "", STRIPE_CONFERENCE_LARGE_PRICE: "" });
const app = express();
app.use("/webhook", routes.billingWebhook);
app.use(express.json());
+45
View File
@@ -35,8 +35,33 @@ describe("paid access policy", () => {
}
expect(() => validateBillingExpiration({ expirationMode: "never" })).not.to.throw();
expect(() => validateBillingExpiration({ expirationMode: "remove", expirationDate: new Date() })).not.to.throw();
// The free window is FREE_EXPIRATION_MONTHS (5) months, not a year.
const sevenMonths = new Date(); sevenMonths.setMonth(sevenMonths.getMonth() + 7);
expect(() => validateBillingExpiration({ expirationMode: "remove", expirationDate: sevenMonths })).to.throw("invalid_billing_expiration");
// An already saved later date stays valid, and a paid limit allows it.
expect(() => validateBillingExpiration({ expirationMode: "remove", expirationDate: sevenMonths }, undefined, new Date(sevenMonths))).not.to.throw();
const limit = new Date(); limit.setFullYear(limit.getFullYear() + 2);
expect(() => validateBillingExpiration({ expirationMode: "remove", expirationDate: sevenMonths }, limit)).not.to.throw();
} finally { config.BILLING_ENABLED = enabled; }
});
it("summarizes purchases without internal IDs or the no-end sentinel", async () => {
const { purchaseHistory } = require("../src/server/routes/billing");
const never = new Date("9999-12-31T00:00:00Z"), later = new Date(Date.now() + 86400000);
const rows = await purchaseHistory([
{ _id: "cs_1:repo_slot", planId: "repo_slot", scope: "repo:paper", status: "paid", until: never },
{ _id: "cs_1:author_pass", planId: "author_pass", scope: "repo:paper", status: "paid", until: later },
{ _id: "cs_1:repo_extend", planId: "repo_extend", scope: "repo:paper", status: "paid", until: later },
{ _id: "cs_2", planId: "repo_never", scope: "repo:other", status: "paid", until: never, revoked: true, revocationReason: "full_refund" },
{ _id: "sub_1", planId: "author_monthly", scope: "user:me", status: "past_due", until: later },
]);
expect(rows).to.have.length(3);
const pack = rows.find(r => r.planId === "repo_pack");
expect(pack).to.include({ repoId: "paper", status: "active", until: null });
expect(pack.parts.find(p => p.planId === "repo_slot").until).to.equal(null);
expect(rows.find(r => r.planId === "repo_never")).to.include({ status: "refunded", until: null });
expect(rows.find(r => r.planId === "author_monthly")).to.include({ status: "overdue", recurring: true, repoId: null });
expect(JSON.stringify(rows)).not.to.include("cs_");
});
it("rejects unsafe configured redirect origins", () => {
const original = config.BILLING_ORIGIN;
try {
@@ -87,6 +112,26 @@ describe("retired conference offers", () => {
expect(model.status).to.equal("ready");
expect(model.plan.planID).to.equal("conference");
});
it("only offers the paid review package when billing is enabled", async () => {
const { offeredConferencePlans } = require("../src/server/routes/conference");
config.BILLING_ENABLED = true; config.STRIPE_CONFERENCE_PRICE = "price_conf";
expect(offeredConferencePlans().map(p => p.id)).to.deep.equal(["conference"]);
config.STRIPE_CONFERENCE_SMALL_PRICE = "price_small"; config.STRIPE_CONFERENCE_LARGE_PRICE = "price_large";
expect(offeredConferencePlans().map(p => p.id)).to.deep.equal(["conference_workshop", "conference", "conference_large"]);
expect(offeredConferencePlans().find(p => p.id === "conference_workshop").repositories).to.equal(config.PAID_CONFERENCE_SMALL_REPOSITORIES);
expect(offeredConferencePlans()[0]).to.not.have.property("priceId");
config.STRIPE_CONFERENCE_SMALL_PRICE = ""; config.STRIPE_CONFERENCE_LARGE_PRICE = "";
config.BILLING_ENABLED = false;
expect(offeredConferencePlans().map(p => p.id)).to.deep.equal(["free_conference"]);
});
it("rejects new free conferences when billing is enabled", async () => {
config.BILLING_ENABLED = true; config.STRIPE_CONFERENCE_PRICE = "price_conf";
body.plan.planID = "free_conference";
let error;
try { await save({ params: {}, body }, { send: () => {} }); } catch (e) { error = e; }
expect(error?.message).to.equal("invalid_plan");
expect(saves).to.equal(0);
});
for (const change of ["new", "plan", "dates"]) {
it(`still rejects ${change} purchases or locked changes`, async () => {
const params = change === "new" ? {} : { conferenceID: "CONF" };
+11 -4
View File
@@ -66,9 +66,14 @@ describe("repository activity dates", () => {
expect(saved.source.commit).to.equal("abc123");
expect(saved).not.to.have.property("publishedAt");
});
function freeLimit() {
const expiration = require("../src/core/billing/expiration");
stub(expiration, "maximumExpiration", async () => expiration.freeExpirationLimit());
}
it("allows privacy-only saves after hosting capacity or conference admission ends", async () => {
const repo = repository(); auth(repo);
stub(require("../src/config").default, "BILLING_ENABLED", true);
freeLimit();
const quotas = require("../src/core/billing/quotas");
repo.model.billingManaged = true;
repo.model.conference = "ended-conf";
@@ -86,6 +91,7 @@ describe("repository activity dates", () => {
it("rejects an oversized edited source before removing the published files", async () => {
const repo = repository(); auth(repo); repo.model.billingManaged = true;
stub(require("../src/config").default, "BILLING_ENABLED", true);
freeLimit();
const quotas = require("../src/core/billing/quotas");
const access = require("../src/core/github-app");
const github = require("../src/core/source/GitHubRepository");
@@ -124,21 +130,22 @@ describe("repository activity dates", () => {
else expect(saved.$unset).to.have.property("billingNeverExpiresUntil", 1);
});
}
it("rejects repeated extensions beyond one year before changing stored settings", async () => {
it("caps extensions at the repository's limit before changing stored settings", async () => {
const repo = repository(); auth(repo);
stub(require("../src/config").default, "BILLING_ENABLED", true);
freeLimit();
repo.model.options.expirationMode = "remove";
repo.model.options.expirationDate = new Date();
let writes = 0;
stub(Model, "updateOne", () => ({ exec: async () => { writes++; return { matchedCount: 1 }; } }));
const extend = handler("repository-private", "/:repoId/extend", "post");
await extend({}, { json: () => {} });
await extend({}, { json: () => {} });
const savedDate = repo.model.options.expirationDate;
expect(savedDate.getTime()).to.equal(require("../src/core/billing/expiration").freeExpirationLimit().getTime());
let error;
try { await extend({}, { json: () => {} }); } catch (caught) { error = caught; }
expect(error?.message).to.equal("invalid_billing_expiration");
expect(writes).to.equal(2);
expect(error?.message).to.equal("expiration_limit_reached");
expect(writes).to.equal(1);
expect(repo.model.options.expirationDate).to.deep.equal(savedDate);
});
it("does not extend an expired submission in a conference that no longer admits it", async () => {
+92 -3
View File
@@ -244,11 +244,12 @@ describe("Vue 3 UI", function () {
ui.window.document.querySelector(".nav-upgrade").click();
await delay(10);
expect(ui.window.document.activeElement.id).to.equal("settings-plan-comparison");
expect(ui.window.document.querySelector(".settings-comparison").textContent).to.include("Scheduled or never");
expect(ui.window.document.querySelector(".settings-comparison").textContent).to.include("Latest expiration");
expect(ui.window.document.querySelector(".settings-comparison").textContent).to.include("Automatic updates");
expect(ui.window.document.querySelectorAll(".settings-nav button")[1].getAttribute("aria-current")).to.equal("page");
expect(ui.window.document.querySelector("#settings-billing").textContent).to.include("/ month");
expect(ui.window.document.querySelector("#settings-billing").textContent).to.include("10 GiB");
ui.window.document.querySelector(".billing-plan button").click();
[...ui.window.document.querySelectorAll(".billing-offer .btn")].find(b => b.textContent.trim() === "Subscribe").click();
await delay(30);
const request = ui.requests.find(r => r.url.pathname === "/api/billing/checkout");
expect(request.payload).to.deep.equal({ planId: "author_monthly", target: "" });
@@ -257,6 +258,32 @@ describe("Vue 3 UI", function () {
expect(ui.errors).to.deep.equal([]);
});
it("lists purchases in plain words, grouped as active and ended", async () => {
ui = await browser("/profile?billing=portal", {
"/api/user": { username: "author" },
"/api/billing/": { enabled: true, csrf: "billing-token", portal: true, plans: [], allowance: { repositories: 5, storage: 512 },
grants: [{ planId: "author_annual", status: "active", until: "2099-01-01", cancelAtPeriodEnd: true }],
purchases: [
{ id: "a", planId: "repo_pack", repoId: "paper-repo", status: "active", until: null, purchasedAt: "2026-01-02", amount: 500, currency: "eur",
parts: [{ planId: "repo_slot", until: null, status: "active" }, { planId: "author_pass", until: "2099-01-01", status: "active" }, { planId: "repo_extend", until: "2099-01-01", status: "active" }] },
{ id: "b", planId: "repo_never", repoId: "old-repo", status: "refunded", until: null, parts: [] },
] },
"/api/billing/refresh": { ok: true, pending: false },
});
await delay(100);
const text = ui.window.document.querySelector("#settings-billing").textContent;
expect(text).to.include("Paper pack");
expect(text).to.include("Extra repository · Live updates until");
expect(text).to.include("Refunded");
expect(text).not.to.include("9999");
expect(text).not.to.include("repo:");
// Returning from the billing portal is not a pending payment.
expect(text).not.to.include("Confirming your payment");
expect(text).to.include("Renewal cancelled. Your access continues until");
expect(ui.window.document.querySelector('a[href="/anonymize/paper-repo"]')).not.to.equal(null);
expect(ui.errors).to.deep.equal([]);
});
it("refreshes the quota panel when checkout confirmation changes the allowance", async () => {
let confirmed = false;
let quotaReads = 0;
@@ -518,7 +545,7 @@ describe("Vue 3 UI", function () {
expect(doc.querySelector('.anonymize-preview-body').textContent).to.include("MASKED");
expect(doc.querySelector('.anonymize-preview-body').textContent).not.to.include("Private Author");
state.options.expirationMode = "never"; await delay(20);
expect(doc.querySelector('.subscription-choice').textContent).to.include("20 repositories");
expect(doc.querySelector('.hosting-options').textContent).to.include("20 repositories");
expect(doc.querySelector('.anonymize-submit-bar').textContent).to.include("Continue to checkout");
doc.querySelector('.preview-expand').click(); await delay(10);
expect(doc.querySelector('.anonymize-page').classList.contains('preview-expanded')).to.equal(true);
@@ -567,6 +594,34 @@ describe("Vue 3 UI", function () {
});
}
it("opens Checkout for the subscription cycle the hosting option shows", async () => {
const billing = { ...taskBilling, plans: [...taskBilling.plans,
{ id: "author_annual", amount: 4900, currency: "eur", interval: "year", repositories: 20, storage: 10737418240, description: "Renews yearly." }] };
ui = await browser("/anonymize", { ...repositorySource, "/api/billing/": billing });
await delay(20);
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state; state.detectedType = "repo";
state.hostingQuotaBlocked = true;
expect(state.hostingOneTimePlans()).to.deep.equal([]);
expect(state.hostingSubscriptionPlan().id).to.equal("author_annual");
expect(state.hostingChoice()).to.equal("author_annual");
expect(state.selectedHostingPlan().id).to.equal("author_annual");
});
it("leaves gist and pull request expiration dates outside the repository billing limit", async () => {
ui = await browser("/anonymize", { ...repositorySource, "/api/billing/": taskBilling });
await delay(20);
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state;
const later = new Date(); later.setMonth(later.getMonth() + 10);
state.detectedType = "gist";
state.options.expirationDate = new ui.window.Date(later.getTime());
await delay(20);
expect(state.options.expirationDate.getTime()).to.equal(later.getTime());
expect(new Date(state.maxExpirationDate).getTime()).to.be.greaterThan(later.getTime());
state.detectedType = "repo";
await delay(20);
expect(state.options.expirationDate.getTime()).to.be.lessThan(later.getTime());
});
it("keeps free, conference and coauthor work out of author checkout", async () => {
ui = await browser("/anonymize", { ...repositorySource, "/api/billing/": taskBilling });
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state; state.detectedType = "repo";
@@ -577,6 +632,9 @@ describe("Vue 3 UI", function () {
state.hostingQuotaBlocked = true;
expect(state.hostingNeedsPayment()).to.equal(true);
state.conference = "CONF";
state.conference_data = { name: "Unpaid", hosting: false };
expect(state.hostingNeedsPayment()).to.equal(true);
state.conference_data = { name: "Paid", hosting: true };
expect(state.hostingNeedsPayment()).to.equal(false);
state.options.expirationMode = "never";
expect(state.hostingNeedsPayment()).to.equal(true);
@@ -934,6 +992,37 @@ describe("Vue 3 UI", function () {
expect(ui.errors).to.deep.equal([]);
});
it("offers a paid review package per venue size for a new conference", async () => {
ui = await browser("/conference/new", {
"/api/user": { username: "owner" },
"/api/conferences/plans": [
{ id: "conference_workshop", name: "Workshop", pricePerRepo: 0, repositories: 25, storagePerRepo: 10737418240 },
{ id: "conference", name: "Conference", pricePerRepo: 0, repositories: 100, storagePerRepo: 53687091200 },
{ id: "conference_large", name: "Large conference", pricePerRepo: 0, repositories: 400, storagePerRepo: 214748364800 }],
"/api/billing/": { enabled: true, grants: [], plans: [
{ id: "conference_workshop", amount: 4900, currency: "eur", repositories: 25, storage: 10737418240 },
{ id: "conference", amount: 14900, currency: "eur", repositories: 100, storage: 53687091200 },
{ id: "conference_large", amount: 39900, currency: "eur", repositories: 400, storage: 214748364800 }] },
});
await delay(30);
const doc = ui.window.document;
const tiers = [...doc.querySelectorAll(".conference-tier")];
expect(tiers.map(t => t.querySelector(".conference-tier-head strong").textContent)).to.deep.equal(["Workshop", "Conference", "Large conference"]);
expect(tiers[0].textContent).to.include("49").and.include("Up to 25 papers");
expect(doc.querySelector('input[name="planPicker"]:checked').value).to.equal("conference");
expect(doc.querySelector("#conf-plan").textContent).to.not.include("Free");
doc.querySelector('input[value="conference_workshop"]').click();
await delay(20);
expect(tiers[0].classList.contains("is-selected")).to.equal(true);
await ui.input("#name", "Review venue");
await ui.input("#conferenceID", "REVIEW26");
doc.querySelector("#send").click();
await delay(30);
const request = ui.requests.find(r => r.method === "POST" && r.url.pathname === "/api/conferences/");
expect(request?.payload.plan.planID).to.equal("conference_workshop");
expect(ui.errors).to.deep.equal([]);
});
it("submits billing details for a new paid conference", async () => {
ui = await browser("/conference/new", {
"/api/user": { username: "owner" },