Download index.html from getreadystack/openiddict-migration-check: direct link, hf CLI and curl.
- Browser
- Download file 45.7 kB
-
https://huggingface.co/spaces/getreadystack/openiddict-migration-check/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/openiddict-migration-check/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/openiddict-migration-check/resolve/main/index.html
45.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Duende Migration 2026 — check your code for free</title> | |
| <meta name="description" content="This free browser tool maps Duende IdentityServer and IdentityServer4 calls in Program.cs or .csproj to OpenIddict equivalents and prices the Duende edition your setup needs today."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/openiddict-migration-check"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static;justify-content:flex-end;margin:0 0 10px}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Duende Migration 2026 — check your code for free</h1> | |
| <p class="lede">Maps every Duende IdentityServer / IdentityServer4 call in Program.cs or .csproj to its OpenIddict equivalent and prices the Duende edition your setup needs today. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">using Duende.IdentityServer; | |
| using Duende.IdentityServer.Models; | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Services.AddRazorPages(); | |
| builder.Services.AddIdentityServer(options => | |
| { | |
| options.Events.RaiseErrorEvents = true; | |
| options.Events.RaiseFailureEvents = true; | |
| options.EmitStaticAudienceClaim = true; | |
| }) | |
| .AddInMemoryIdentityResources(Config.IdentityResources) | |
| .AddInMemoryApiScopes(Config.ApiScopes) | |
| .AddInMemoryClients(Config.Clients) | |
| .AddServerSideSessions() | |
| .AddTestUsers(TestUsers.Users); | |
| var app = builder.Build(); | |
| app.UseStaticFiles(); | |
| app.UseRouting(); | |
| app.UseIdentityServer(); | |
| app.UseAuthorization(); | |
| app.MapRazorPages(); | |
| app.Run(); | |
| public static class Config | |
| { | |
| public static IEnumerable<IdentityResource> IdentityResources => | |
| new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile() }; | |
| public static IEnumerable<ApiScope> ApiScopes => | |
| new ApiScope[] { new ApiScope("orders.read"), new ApiScope("orders.write") }; | |
| public static IEnumerable<Client> Clients => | |
| new Client[] | |
| { | |
| new Client | |
| { | |
| ClientId = "billing-worker", | |
| ClientSecrets = { new Secret("worker-secret".Sha256()) }, | |
| AllowedGrantTypes = GrantTypes.ClientCredentials, | |
| AllowedScopes = { "orders.read" } | |
| }, | |
| new Client | |
| { | |
| ClientId = "web", | |
| ClientSecrets = { new Secret("web-secret".Sha256()) }, | |
| AllowedGrantTypes = GrantTypes.Code, | |
| RedirectUris = { "https://shop.example.com/signin-oidc" }, | |
| AllowedScopes = { "openid", "profile", "orders.read", "orders.write" } | |
| }, | |
| new Client | |
| { | |
| ClientId = "mobile", | |
| RequireClientSecret = false, | |
| AllowedGrantTypes = GrantTypes.Code, | |
| RedirectUris = { "com.example.shop:/callback" }, | |
| AllowedScopes = { "openid", "profile", "orders.read" } | |
| } | |
| }; | |
| }</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Scan one Program.cs or .csproj: every Duende call mapped to its OpenIddict replacement, plus the Duende edition and list price your setup needs.. <strong>In the editor:</strong> Whole-solution scan across every project, with a migration plan export (Markdown/CSV) per project for the pull request and the licence review. — that part asks for a licence key.</div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var OIDM_RULES = [ | |
| {"id": "duende-licence", "sev": "error", "re": "\\bDuende\\.IdentityServer\\b", "msg": "Duende IdentityServer package or namespace: production use needs a paid Duende licence unless you are Community-eligible (under 1M USD revenue and under 3M USD capital)", "oi": "OpenIddict.AspNetCore + OpenIddict.EntityFrameworkCore (Apache-2.0, no licence fee); swap the PackageReference and the Duende.IdentityServer.* usings"}, | |
| {"id": "is4-eol", "sev": "error", "kind": "eol", "eol": "2022-12-13", "re": "\\bIdentityServer4\\b", "msg": "IdentityServer4 has had no security fixes since 2022-12-13 ({days} days as of {today})", "oi": "migrate straight to OpenIddict instead of paying for the Duende upgrade"}, | |
| {"id": "host-wiring", "sev": "warning", "re": "\\b(AddIdentityServer|UseIdentityServer)\\s*\\(", "msg": "IdentityServer host wiring ({found})", "oi": "services.AddOpenIddict().AddCore().AddServer().AddAspNetCore() and app.UseAuthentication(); OpenIddict enables no endpoint by default, so call SetTokenEndpointUris(\"connect/token\") and SetAuthorizationEndpointUris(\"connect/authorize\") to keep the URLs your clients call"}, | |
| {"id": "license-key", "sev": "info", "re": "\\bLicenseKey\\b|Duende_License\\.key", "msg": "Duende licence key wiring ({found})", "oi": "delete it; OpenIddict has no licence key"}, | |
| {"id": "inmemory-config", "sev": "warning", "re": "\\bAddInMemory(Clients|ApiScopes|ApiResources|IdentityResources)\\b", "msg": "In-memory Duende configuration ({found})", "oi": "seed clients with IOpenIddictApplicationManager.CreateAsync(new OpenIddictApplicationDescriptor { ... }) and scopes with IOpenIddictScopeManager; each AllowedGrantTypes value becomes a Permissions.GrantTypes.* entry and each flow must be switched on (AllowAuthorizationCodeFlow(), AllowClientCredentialsFlow())"}, | |
| {"id": "ef-stores", "sev": "warning", "re": "\\bAdd(ConfigurationStore|OperationalStore)\\b", "msg": "Duende EF Core stores ({found})", "oi": "options.UseEntityFrameworkCore().UseDbContext\u003cAppDbContext\u003e() plus builder.UseOpenIddict() in the DbContext; the ConfigurationDb and PersistedGrantDb tables do not carry over, so copy clients and scopes with a one-off script"}, | |
| {"id": "aspnet-identity", "sev": "warning", "re": "\\bAddAspNetIdentity\\s*\u003c", "msg": "AddAspNetIdentity\u003cTUser\u003e() bridge", "oi": "no bridge package: sign users in with ASP.NET Core Identity inside your own /connect/authorize action (EnableAuthorizationEndpointPassthrough()) and return SignIn(principal)"}, | |
| {"id": "dev-signing", "sev": "info", "re": "\\bAddDeveloperSigningCredential\\s*\\(", "msg": "Developer signing credential", "oi": "AddDevelopmentEncryptionCertificate() + AddDevelopmentSigningCertificate(); OpenIddict encrypts access tokens by default, so APIs that validate plain JWTs need DisableAccessTokenEncryption() or the OpenIddict validation handler"}, | |
| {"id": "signing-cred", "sev": "info", "re": "\\b(AddSigningCredential|AddValidationKey)\\s*\\(", "msg": "Signing key registration ({found})", "oi": "AddSigningCertificate(cert) plus AddEncryptionCertificate(cert); OpenIddict needs both"}, | |
| {"id": "key-management", "sev": "warning", "tier": "akm", "re": "\\bAddIdentityServer\\s*\\(", "unless": "KeyManagement\\s*\\.\\s*Enabled\\s*=\\s*false|\\bIdentityServer4\\b", "msg": "Automatic key management left on (no KeyManagement.Enabled = false): Advanced edition, or the 4,000 USD/yr add-on on Standard", "oi": "OpenIddict does not generate or rotate keys; load signing and encryption certificates yourself and add the next one before the current one expires"}, | |
| {"id": "server-side-sessions", "sev": "warning", "tier": "standard", "re": "\\bAddServerSideSessions\\b", "msg": "Server-side sessions: Standard edition or higher", "oi": "no built-in equivalent; keep the login session in the ASP.NET Core cookie with a custom ITicketStore (OpenIddict stores tokens and authorizations, not login sessions)"}, | |
| {"id": "profile-service", "sev": "info", "re": "\\bIProfileService\\b|\\bAddProfileService\\s*\u003c", "msg": "IProfileService claim mapping", "oi": "set claims on the ClaimsPrincipal in your authorize/token handler and route each one with SetDestinations()"}, | |
| {"id": "bff", "sev": "warning", "re": "\\bDuende\\.Bff\\b|\\bAddBff\\s*\\(", "msg": "Duende BFF is licensed separately; without a key BFF v4 runs in trial mode capped at 5 sessions per host", "oi": "no OpenIddict package; use ASP.NET Core cookie auth with the OpenIddict client and proxy API calls through YARP"}, | |
| {"id": "dcr", "sev": "warning", "tier": "standard", "re": "\\bAddIdentityServerConfiguration\\s*\\(|\\bMapDynamicClientRegistration\\b", "msg": "Dynamic client registration: Standard edition or higher", "oi": "OpenIddict ships no registration endpoint; expose your own admin API over IOpenIddictApplicationManager"}, | |
| {"id": "ciba", "sev": "warning", "tier": "standard", "re": "\\b(IBackchannelAuthenticationUserValidator|IBackchannelAuthenticationUserNotificationService|AddBackchannelAuthenticationUserValidator)\\b", "msg": "CIBA backchannel login ({found}): Standard edition or higher", "oi": "OpenIddict has no CIBA endpoint; move the flow to the device authorization flow or keep it on a separate server"}, | |
| {"id": "mtls", "sev": "warning", "tier": "standard", "re": "\\bMutualTls\\s*\\.\\s*Enabled\\s*=\\s*true", "msg": "Mutual TLS client authentication: Standard edition or higher", "oi": "confirm the OpenIddict version you target supports mTLS client authentication before cutover and keep the certificate forwarding middleware"}, | |
| {"id": "dynamic-providers", "sev": "warning", "tier": "advanced", "re": "\\b(AddIdentityProviderStore|AddInMemoryOidcProviders)\\b", "msg": "Dynamic identity providers ({found}): Advanced edition", "oi": "register each upstream login with AddOpenIdConnect() or the OpenIddict client at startup; OpenIddict has no runtime provider store"}, | |
| {"id": "edition", "sev": "error", "kind": "edition", "when": ["duende-licence", "host-wiring"], "client_re": "\\bnew\\s+Client\\s*[({]", "akm_addon": 4000, | |
| "editions": [{"name": "Lite", "rank": 1, "price": 5750, "clients": 2}, {"name": "Standard", "rank": 2, "price": 12500, "clients": 10}, {"name": "Advanced", "rank": 3, "price": 24900, "clients": 30}], | |
| "msg": "Running this setup on Duende: {clients} + {features} = cheapest edition {edition}, {price} USD/yr list price", "oi": "Apache-2.0, no licence fee at any revenue"} | |
| ] | |
| ;</script> | |
| <script>/* OpenIddict Migration Check engine: maps Duende IdentityServer / IdentityServer4 calls to OpenIddict and prices the Duende edition a setup needs. Runs in node and in the browser. */ | |
| (function () { | |
| const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.OIDM_RULES; | |
| // Blank out comments but keep newlines so line numbers stay true. "://" inside URLs is not a comment. | |
| function stripComments(text) { | |
| const keepLines = (m) => m.replace(/[^\n]/g, ' '); | |
| return String(text || '') | |
| .replace(/\/\*[\s\S]*?\*\//g, keepLines) | |
| .replace(/<!--[\s\S]*?-->/g, keepLines) | |
| .replace(/(^|[^:])(\/\/[^\n]*)/g, (m, a, b) => a + keepLines(b)); | |
| } | |
| function lineOf(src, idx) { return src.slice(0, idx).split('\n').length; } | |
| function parseDay(v) { | |
| const m = String(v || '').match(/(\d{4})-(\d{2})-(\d{2})/); | |
| if (!m) return null; | |
| const t = Date.UTC(+m[1], +m[2] - 1, +m[3]); | |
| return isNaN(t) ? null : t; | |
| } | |
| function money(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } | |
| function fill(tpl, vars) { return tpl.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? vars[k] : m)); } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| const src = stripComments(text); | |
| const todayT = parseDay(opts.today) ?? parseDay(new Date().toISOString()); | |
| const today = new Date(todayT).toISOString().slice(0, 10); | |
| const findings = []; | |
| const fired = {}; | |
| for (const r of RULES) { | |
| if (r.kind === 'edition') continue; | |
| const re = new RegExp(r.re, 'g'); | |
| const hits = [...src.matchAll(re)]; | |
| if (!hits.length) continue; | |
| if (r.unless && new RegExp(r.unless).test(src)) continue; | |
| const found = [...new Set(hits.map((h) => h[1] || h[0].replace(/\s*[(<]$/, '')))].join(', '); | |
| const vars = { found, today }; | |
| if (r.kind === 'eol') vars.days = Math.floor((todayT - parseDay(r.eol)) / 864e5); | |
| const line = lineOf(src, hits[0].index); | |
| fired[r.id] = { line, tier: r.tier, label: r.msg.split(/[:(]/)[0].trim() }; | |
| findings.push({ check: r.id, sev: r.sev, msg: fill(r.msg, vars) + ' → OpenIddict: ' + r.oi, line }); | |
| } | |
| const ed = RULES.find((r) => r.kind === 'edition'); | |
| const trigger = ed && ed.when.map((id) => fired[id]).find(Boolean); | |
| if (trigger) { | |
| const n = (src.match(new RegExp(ed.client_re, 'g')) || []).length; | |
| const tiers = Object.values(fired).filter((f) => f.tier); | |
| const need = Math.max(1, ...tiers.map((f) => (f.tier === 'advanced' ? 3 : 2))); | |
| const akm = tiers.some((f) => f.tier === 'akm'); | |
| let pick = null; | |
| for (const e of ed.editions) { | |
| if (e.rank < need || n > e.clients) continue; | |
| if (akm && e.rank < 2) continue; | |
| const price = e.price + (akm && e.rank === 2 ? ed.akm_addon : 0); | |
| const name = e.name + (akm && e.rank === 2 ? ' + key management add-on' : ''); | |
| if (!pick || price < pick.price) pick = { name, price }; | |
| } | |
| const clients = n ? n + ' client' + (n > 1 ? 's' : '') : 'client count not in this file'; | |
| const features = tiers.length ? tiers.map((f) => f.label).join(', ') : 'core protocol only'; | |
| findings.push({ | |
| check: ed.id, sev: ed.sev, line: trigger.line, | |
| msg: pick | |
| ? fill(ed.msg, { clients, features, edition: pick.name, price: money(pick.price) }) + ' → OpenIddict: ' + ed.oi | |
| : 'Running this setup on Duende: ' + clients + ' + ' + features + ' is past the Advanced edition (30 clients), so the Custom edition is quoted by Duende → OpenIddict: ' + ed.oi | |
| }); | |
| } | |
| return { findings }; | |
| } | |
| const api = { engine: { check }, RULES, RULE_COUNT: RULES.length }; | |
| if (typeof module !== 'undefined') module.exports = api; | |
| if (typeof window !== 'undefined') window.OIDMENGINE = api; | |
| })(); | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var OIDMREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = OIDMREPORT; | |
| else window.OIDMREPORT = OIDMREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.OIDMENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.OIDMREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/openiddict-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'openiddict-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind OpenIddict Migration Check: Duende IdentityServer every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"openiddict-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Whole-solution scan across every project, with a migration plan export (Markdown/CSV) per project for the pull request and the licence review.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Duende IdentityServer list price: Lite 5,750, Standard 12,500, Advanced 24,900 USD a year (duendesoftware.com/pricing)</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_Sy20q9ra2qXABCf5XU9jA1EaHat860YV0kFiX4SaDW0" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-30">Updated 2026-09-30</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-30","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku397412_result_card.jpg" alt="Migrate from Duende IdentityServer to OpenIddict: price your Program.cs before the renewal" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>6 findings and a 16,500 USD-a-year Duende bill: that is what one .NET team's Program.cs returns when a backend developer runs it through OpenIddict Migration Check, a VS Code extension with 18 rules that maps every Duende IdentityServer call to its OpenIddict replacement.</p> | |
| <h3>What Duende changed, in Duende's own terms</h3> | |
| <p>Duende IdentityServer is free only as the Community edition, for for-profit companies with "less than $1M USD projected annual gross revenue and access to less than $3M USD in capital facilities" (duendesoftware.com/products/communityedition). Everyone else pays yearly. The list prices on duendesoftware.com/pricing, checked 2026-09-29, are Lite 5,750 USD (2 client IDs), Standard 12,500 USD (10 client IDs) and Advanced 24,900 USD (30 client IDs). Automatic key management sits in Advanced, or as a 4,000 USD add-on on Standard. The free predecessor, IdentityServer4, reached end of support on 2022-12-13 and gets no security fixes.</p> | |
| <p>OpenIddict is Apache-2.0. The question every team above the Community line has to answer before the renewal date is simple: what does our setup cost on Duende, and what breaks if we move?</p> | |
| <h3>The sample file</h3> | |
| <p>The sample is the Program.cs most Duende projects start from: AddIdentityServer with in-memory identity resources, API scopes and clients, server-side sessions switched on, and three clients (a client-credentials worker, a web app and a mobile app). The check reports:</p> | |
| <ol><li>duende-licence: the Duende.IdentityServer namespace. Swap to OpenIddict.AspNetCore and OpenIddict.EntityFrameworkCore.</li><li>host-wiring: AddIdentityServer and UseIdentityServer. Replace with AddOpenIddict().AddCore().AddServer(). OpenIddict enables no endpoint by default, so set SetTokenEndpointUris("connect/token") or every client gets a 404.</li><li>inmemory-config: the in-memory clients and scopes. Seed them through IOpenIddictApplicationManager; each AllowedGrantTypes value becomes a Permissions.GrantTypes entry, and each flow must be switched on.</li><li>key-management: KeyManagement.Enabled = false is missing, so automatic key management stays in use. OpenIddict does not generate or rotate keys; you load certificates yourself.</li><li>server-side-sessions: a Standard feature. OpenIddict has no equivalent; keep the login session in the ASP.NET Core cookie with a custom ITicketStore.</li><li>edition: 3 clients plus those two features means the cheapest Duende option is Standard plus the key management add-on, 16,500 USD a year at list price.</li></ol> | |
| <h3>What changes with other inputs</h3> | |
| <p>A two-client setup with KeyManagement.Enabled = false and its own signing credential drops to Lite at 5,750 USD a year. An IdentityServer4 app with 12 clients gets an end-of-support warning with the day count, and the 12 clients alone push a Duende upgrade to Advanced at 24,900 USD.</p> | |
| <h3>What a chatbot misses</h3> | |
| <p>Ask a chatbot to port AddIdentityServer and it will write an AddOpenIddict block. It will not tell you that OpenIddict encrypts access tokens by default, so APIs validating plain JWTs need DisableAccessTokenEncryption() or the OpenIddict validation handler. It will not count your clients against Duende's tiers. The check reads your file, so the list is yours.</p> | |
| <h3>Try it</h3> | |
| <p>The free scan works on the open file in VS Code or in the web version at getreadystack.com/tools/openiddict-migration-check, with no key and no limit. The full version scans every project in a solution and exports a migration plan per project.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku397412_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku397412_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does OpenIddict Migration Check do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a Program.cs, Startup.cs or .csproj and flags every Duende IdentityServer or IdentityServer4 call with its OpenIddict replacement, using 18 rules. On the sample Program.cs it reports 6 findings: the Duende licence, host wiring, in-memory configuration, automatic key management, server-side sessions, and the cheapest Duende edition for that setup, 16,500 USD a year.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">.NET teams running Duende IdentityServer or the unsupported IdentityServer4 who are above the Duende Community edition limit of 1M USD annual revenue or 3M USD capital, and who must decide before the next renewal whether to pay for a Duende edition or move the token server to OpenIddict.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask a chatbot or read the OpenIddict samples?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A chatbot rewrites one call at a time and does not know which Duende edition your features belong to. The samples show a fresh OpenIddict server, not your file. This check reads your actual code, names each Duende feature it finds, prices the edition it needs and lists the OpenIddict defaults that break clients, such as endpoints off by default.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what does the full version add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: scan the open file, with every finding, its OpenIddict replacement and the Duende edition price, with no key and no limit. The full version, a one-time payment, scans every project in the solution at once and exports a migration plan per project as Markdown or CSV for the pull request and the licence review.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does staying on Duende cost compared to this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Duende IdentityServer list prices are Lite 5,750 USD a year for 2 clients, Standard 12,500 USD for 10 clients and Advanced 24,900 USD for 30 clients, with automatic key management a 4,000 USD add-on on Standard. OpenIddict is Apache-2.0 with no licence fee. The check shows which of those prices your file needs.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does OpenIddict Migration Check do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a Program.cs, Startup.cs or .csproj and flags every Duende IdentityServer or IdentityServer4 call with its OpenIddict replacement, using 18 rules. On the sample Program.cs it reports 6 findings: the Duende licence, host wiring, in-memory configuration, automatic key management, server-side sessions, and the cheapest Duende edition for that setup, 16,500 USD a year."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": ".NET teams running Duende IdentityServer or the unsupported IdentityServer4 who are above the Duende Community edition limit of 1M USD annual revenue or 3M USD capital, and who must decide before the next renewal whether to pay for a Duende edition or move the token server to OpenIddict."}}, {"@type": "Question", "name": "Why not just ask a chatbot or read the OpenIddict samples?", "acceptedAnswer": {"@type": "Answer", "text": "A chatbot rewrites one call at a time and does not know which Duende edition your features belong to. The samples show a fresh OpenIddict server, not your file. This check reads your actual code, names each Duende feature it finds, prices the edition it needs and lists the OpenIddict defaults that break clients, such as endpoints off by default."}}, {"@type": "Question", "name": "What is free and what does the full version add?", "acceptedAnswer": {"@type": "Answer", "text": "Free: scan the open file, with every finding, its OpenIddict replacement and the Duende edition price, with no key and no limit. The full version, a one-time payment, scans every project in the solution at once and exports a migration plan per project as Markdown or CSV for the pull request and the licence review."}}, {"@type": "Question", "name": "What does staying on Duende cost compared to this?", "acceptedAnswer": {"@type": "Answer", "text": "Duende IdentityServer list prices are Lite 5,750 USD a year for 2 clients, Standard 12,500 USD for 10 clients and Advanced 24,900 USD for 30 clients, with automatic key management a 4,000 USD add-on on Standard. OpenIddict is Apache-2.0 with no licence fee. The check shows which of those prices your file needs."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:397412,slug:"openiddict-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/dotnet-eol-support-gate"><span class="yj-rel-t">.NET EOL & CRA Support Window Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/almalinux-migration-lint"><span class="yj-rel-t">AlmaLinux Migration Lint — CentOS 7/8</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/brave-search-api-migration-check"><span class="yj-rel-t">Brave Search API Migration Check — Google Custom Search & Bing</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cinc-migration-check"><span class="yj-rel-t">Cinc Migration Check: Chef Infra licence gate lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/crashlytics-migration-lint"><span class="yj-rel-t">Crashlytics Migration Lint for App Center</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/docker-hub-pull-limit-lint"><span class="yj-rel-t">Docker Hub Pull Limit Lint</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |