+ {{ accountDisplayName }} +
++ {{ accountDisplayName }} +
++ via {{ proofMethodLabel }} + + Added {{ formatDate(account.addedAt) }} + + Last checked {{ formatDate(localLastCheckedAt) }} +
+ ++ {{ localFailureReason }} +
+Re-verify Claim
+{{ platformLabel }}
+ ++ {{ reverifyError }} +
+{{ $t('profile.linked_accounts.legend_info_title') }}
++ {{ + $t('profile.linked_accounts.verified_summary', { + verified: verifiedCount, + total: accounts.length, + }) + }} +
++ {{ $t('profile.linked_accounts.empty') }} +
+ +{{ $t('common.error') }}
+{{ $t('profile.likes_error') }}
+{{ $t('profile.likes_empty') }}
+ {{ $t('profile.likes_error') }} +
diff --git a/i18n/locales/ar-EG.json b/i18n/locales/ar-EG.json index 18682843b5..d6626059ef 100644 --- a/i18n/locales/ar-EG.json +++ b/i18n/locales/ar-EG.json @@ -82,7 +82,6 @@ "cancel": "إلغاء", "save": "حفظ", "edit": "تعديل", - "error": "خطأ", "view_on": { "gitlab": "عرض على GitLab", "bitbucket": "عرض على Bitbucket", diff --git a/i18n/locales/az-AZ.json b/i18n/locales/az-AZ.json index 4737416609..0cca81be0d 100644 --- a/i18n/locales/az-AZ.json +++ b/i18n/locales/az-AZ.json @@ -210,7 +210,6 @@ "cancel": "Ləğv et", "save": "Saxla", "edit": "Redaktə et", - "error": "Xəta", "view_on": { "npm": "npm-də bax", "github": "GitHub-da bax" diff --git a/i18n/locales/bg-BG.json b/i18n/locales/bg-BG.json index d289b44e80..df11858456 100644 --- a/i18n/locales/bg-BG.json +++ b/i18n/locales/bg-BG.json @@ -173,7 +173,6 @@ "cancel": "Отказ", "save": "Запазване", "edit": "Редактиране", - "error": "Грешка", "view_on": { "npm": "преглед в npm", "github": "Преглед в GitHub" diff --git a/i18n/locales/bn-IN.json b/i18n/locales/bn-IN.json index af5c83bcd3..d576eb2658 100644 --- a/i18n/locales/bn-IN.json +++ b/i18n/locales/bn-IN.json @@ -366,7 +366,6 @@ "cancel": "বাতিল করুন", "save": "সংরক্ষণ করুন", "edit": "সম্পাদনা করুন", - "error": "ত্রুটি", "view_on": { "npm": "npm এ দেখুন", "github": "GitHub এ দেখুন", diff --git a/i18n/locales/cs-CZ.json b/i18n/locales/cs-CZ.json index 52e946ae33..fb9f0d85e5 100644 --- a/i18n/locales/cs-CZ.json +++ b/i18n/locales/cs-CZ.json @@ -363,7 +363,6 @@ "cancel": "Zrušit", "save": "Uložit", "edit": "Upravit", - "error": "Chyba", "view_on": { "npm": "Zobrazit na npm", "github": "Zobrazit na GitHubu", diff --git a/i18n/locales/de.json b/i18n/locales/de.json index 69f128ff87..56b48ce84c 100644 --- a/i18n/locales/de.json +++ b/i18n/locales/de.json @@ -331,7 +331,6 @@ "cancel": "Abbrechen", "save": "Speichern", "edit": "Bearbeiten", - "error": "Fehler", "view_on": { "npm": "Auf npm ansehen", "github": "Auf GitHub ansehen", diff --git a/i18n/locales/en.json b/i18n/locales/en.json index a09e22b59f..62cf109177 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -374,7 +374,6 @@ "cancel": "Cancel", "save": "Save", "edit": "Edit", - "error": "Error", "view_on": { "npm": "View on npm", "github": "View on GitHub", @@ -402,6 +401,8 @@ "website": "Website", "website_placeholder": "https://example.com", "likes": "Likes", + "likes_error": "Could not load liked packages.", + "likes_empty": "No liked packages yet.", "seo_title": "{handle} - npmx", "seo_description": "npmx profile by {handle}", "not_found": "Profile Not Found", @@ -410,6 +411,23 @@ "message": "It doesn't look like they're using npmx yet. Want to tell them about it?", "share_button": "Share on Bluesky", "compose_text": "Hey {'@'}{handle}! Have you checked out npmx.dev yet? It's a browser for the npm registry that's fast, modern, and open-source.\nhttps://npmx.dev" + }, + "linked_accounts": { + "status": { + "verified": "Verified", + "unverified": "Unverified", + "stale": "Stale", + "failed": "Failed" + }, + "title": "Linked Accounts", + "verified_summary": "{verified}/{total} verified", + "legend_aria_label": "Account verification status legend", + "legend_info_title": "What the badges mean", + "legend_verified": "The claim has been checked and the account is confirmed.", + "legend_unverified": "We couldn't confirm the claim - try re-verifying the account.", + "legend_stale": "The last verification is outdated; re-verify to refresh the status.", + "legend_failed": "Verification failed (see details). You can click \"Re-verify\" on the account to attempt a fresh check.", + "empty": "No linked accounts" } }, "package": { diff --git a/i18n/locales/es.json b/i18n/locales/es.json index b6b545404e..61cbbdca0a 100644 --- a/i18n/locales/es.json +++ b/i18n/locales/es.json @@ -366,7 +366,6 @@ "cancel": "Cancelar", "save": "Guardar", "edit": "Editar", - "error": "Error", "view_on": { "npm": "Ver en npm", "github": "Ver en GitHub", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 12ea672f09..2030844c1a 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -331,7 +331,6 @@ "cancel": "Annuler", "save": "Enregistrer", "edit": "Modifier", - "error": "Erreur", "view_on": { "npm": "voir sur npm", "github": "Voir sur GitHub", diff --git a/i18n/locales/hi-IN.json b/i18n/locales/hi-IN.json index 77ccae8a94..3ca8690049 100644 --- a/i18n/locales/hi-IN.json +++ b/i18n/locales/hi-IN.json @@ -228,7 +228,6 @@ "cancel": "रद्द करें", "save": "सहेजें", "edit": "संपादित करें", - "error": "त्रुटि", "view_on": { "npm": "npm पर देखें", "github": "GitHub पर देखें", diff --git a/i18n/locales/hu-HU.json b/i18n/locales/hu-HU.json index bfb78e3d27..0aae322588 100644 --- a/i18n/locales/hu-HU.json +++ b/i18n/locales/hu-HU.json @@ -173,7 +173,6 @@ "cancel": "Mégse", "save": "Mentés", "edit": "Szerkesztés", - "error": "Hiba", "view_on": { "npm": "megtekintés npm-en", "github": "Megtekintés GitHubon" diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index b21589bd24..36691d1023 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -228,7 +228,6 @@ "cancel": "Batal", "save": "Simpan", "edit": "Edit", - "error": "Kesalahan", "view_on": { "npm": "lihat di npm", "github": "Lihat di GitHub", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index 24f00a8039..4114f72254 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -358,7 +358,6 @@ "cancel": "Annulla", "save": "Salva", "edit": "Modifica", - "error": "Errore", "view_on": { "npm": "vedi su npm", "github": "Vedi su GitHub", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 34f7688d64..1ac0b3ecd0 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -366,7 +366,6 @@ "cancel": "キャンセル", "save": "保存", "edit": "編集", - "error": "エラー", "view_on": { "npm": "npmで表示", "github": "GitHubで表示", diff --git a/i18n/locales/mr-IN.json b/i18n/locales/mr-IN.json index 09e258f33a..22dfdf3a15 100644 --- a/i18n/locales/mr-IN.json +++ b/i18n/locales/mr-IN.json @@ -211,7 +211,6 @@ "cancel": "रद्द करा", "save": "जतन करा", "edit": "संपादित करा", - "error": "त्रुटी", "view_on": { "npm": "npm वर पहा", "github": "GitHub वर पहा", diff --git a/i18n/locales/nb-NO.json b/i18n/locales/nb-NO.json index 2caa3e1c74..268ecb2980 100644 --- a/i18n/locales/nb-NO.json +++ b/i18n/locales/nb-NO.json @@ -363,7 +363,6 @@ "cancel": "Avbryt", "save": "Lagre", "edit": "Rediger", - "error": "Feil", "view_on": { "npm": "vis på npm", "github": "Vis på GitHub", diff --git a/i18n/locales/ne-NP.json b/i18n/locales/ne-NP.json index 4fea8c0f9a..cbe6d6d0b8 100644 --- a/i18n/locales/ne-NP.json +++ b/i18n/locales/ne-NP.json @@ -319,6 +319,7 @@ "slate": "स्लेट", "black": "कालो" }, + "foreground_themes": {}, "keyboard_shortcuts_enabled": "किबोर्ड सर्टकटहरू सक्षम गर्नुहोस्", "keyboard_shortcuts_enabled_description": "अन्य ब्राउजर वा सिस्टम सर्टकटहरूसँग बाझिएमा किबोर्ड सर्टकटहरू निष्क्रिय गर्न सकिन्छ", "enable_code_ligatures": "कोडमा लिगेचरहरू सक्षम गर्नुहोस्", @@ -366,7 +367,6 @@ "cancel": "रद्द गर्नुहोस्", "save": "सेभ गर्नुहोस्", "edit": "सम्पादन गर्नुहोस्", - "error": "त्रुटि", "view_on": { "npm": "npm मा हेर्नुहोस्", "github": "GitHub मा हेर्नुहोस्", @@ -402,6 +402,9 @@ "message": "उहाँले npmx प्रयोग गरिरहनुभएको जस्तो देखिँदैन। उहाँलाई यसबारे बताउन चाहनुहुन्छ?", "share_button": "Bluesky मा सेयर गर्नुहोस्", "compose_text": "नमस्ते {'@'}{handle}! तपाईंले npmx.dev हेर्नुभयो? यो npm रजिस्ट्रीका लागि छिटो, आधुनिक र ओपन सोर्स ब्राउजर हो।\nhttps://npmx.dev" + }, + "linked_accounts": { + "status": {} } }, "package": { diff --git a/i18n/locales/nl.json b/i18n/locales/nl.json index b10109a4aa..e9e1b156ee 100644 --- a/i18n/locales/nl.json +++ b/i18n/locales/nl.json @@ -365,7 +365,6 @@ "cancel": "Annuleer", "save": "Opslaan", "edit": "Wijzigen", - "error": "Fout", "view_on": { "npm": "Bekijk op npm", "github": "Bekijk op GitHub", diff --git a/i18n/locales/pl-PL.json b/i18n/locales/pl-PL.json index 9888a88aaf..0c21a08098 100644 --- a/i18n/locales/pl-PL.json +++ b/i18n/locales/pl-PL.json @@ -210,7 +210,6 @@ "cancel": "Anuluj", "save": "Zapisz", "edit": "Edytuj", - "error": "Błąd", "view_on": { "npm": "zobacz na npm", "github": "Zobacz w GitHub", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index 8be735de9f..3200290582 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -327,7 +327,6 @@ "cancel": "Cancelar", "save": "Salvar", "edit": "Editar", - "error": "Erro", "view_on": { "npm": "Ver no npm", "github": "Ver no GitHub", diff --git a/i18n/locales/pt-PT.json b/i18n/locales/pt-PT.json index 8589219401..f3fbbbb69b 100644 --- a/i18n/locales/pt-PT.json +++ b/i18n/locales/pt-PT.json @@ -328,7 +328,6 @@ "cancel": "Cancelar", "save": "Guardar", "edit": "Editar", - "error": "Erro", "view_on": { "npm": "Ver no npm", "github": "Ver no GitHub", diff --git a/i18n/locales/ro-RO.json b/i18n/locales/ro-RO.json index 6667f08eaf..8256131582 100644 --- a/i18n/locales/ro-RO.json +++ b/i18n/locales/ro-RO.json @@ -328,7 +328,6 @@ "cancel": "Anulează", "save": "Salvează", "edit": "Editează", - "error": "Eroare", "view_on": { "npm": "Vizualizează pe npm", "github": "Vizualizează pe GitHub", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 4770fe640d..47678bd9c8 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -374,7 +374,6 @@ "cancel": "Отменить", "save": "Сохранить", "edit": "Изменить", - "error": "Ошибка", "view_on": { "npm": "Открыть на npm", "github": "Открыть на GitHub", diff --git a/i18n/locales/sr-Latn-RS.json b/i18n/locales/sr-Latn-RS.json index 2c76fa50b9..71eb3481e0 100644 --- a/i18n/locales/sr-Latn-RS.json +++ b/i18n/locales/sr-Latn-RS.json @@ -228,7 +228,6 @@ "cancel": "Otkažite", "save": "Sačuvajte", "edit": "Uredite", - "error": "Greška", "view_on": { "npm": "pogledajte na npm-u", "github": "Pogledajte na GitHub-u", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 911af52729..952206b1c5 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -331,7 +331,6 @@ "cancel": "İptal", "save": "Kaydet", "edit": "Düzenle", - "error": "Hata", "view_on": { "npm": "npm'de görüntüle", "github": "GitHub'da görüntüle", diff --git a/i18n/locales/uk-UA.json b/i18n/locales/uk-UA.json index af398342f5..44a039f322 100644 --- a/i18n/locales/uk-UA.json +++ b/i18n/locales/uk-UA.json @@ -328,7 +328,6 @@ "cancel": "Скасувати", "save": "Зберегти", "edit": "Редагувати", - "error": "Помилка", "view_on": { "npm": "Переглянути на npm", "github": "Переглянути на GitHub", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 1301de2d17..7016f99033 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -228,7 +228,6 @@ "cancel": "Hủy", "save": "Lưu", "edit": "Chỉnh sửa", - "error": "Lỗi", "view_on": { "npm": "xem trên npm", "github": "Xem trên GitHub", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 99d9019d6a..24870c8430 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -328,7 +328,6 @@ "cancel": "取消", "save": "保存", "edit": "编辑", - "error": "加载出错", "view_on": { "npm": "在 npm 上查看", "github": "在 GitHub 上查看", diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json index e2b15eefb7..fa0e7e1e95 100644 --- a/i18n/locales/zh-TW.json +++ b/i18n/locales/zh-TW.json @@ -322,7 +322,6 @@ "cancel": "取消", "save": "儲存", "edit": "編輯", - "error": "錯誤", "view_on": { "npm": "在 npm 上檢視", "github": "在 GitHub 上檢視", diff --git a/i18n/schema.json b/i18n/schema.json index b7793e498c..16f628dee8 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1126,9 +1126,6 @@ "edit": { "type": "string" }, - "error": { - "type": "string" - }, "view_on": { "type": "object", "properties": { @@ -1210,6 +1207,12 @@ "likes": { "type": "string" }, + "likes_error": { + "type": "string" + }, + "likes_empty": { + "type": "string" + }, "seo_title": { "type": "string" }, @@ -1236,6 +1239,57 @@ } }, "additionalProperties": false + }, + "linked_accounts": { + "type": "object", + "properties": { + "status": { + "type": "object", + "properties": { + "verified": { + "type": "string" + }, + "unverified": { + "type": "string" + }, + "stale": { + "type": "string" + }, + "failed": { + "type": "string" + } + }, + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "verified_summary": { + "type": "string" + }, + "legend_aria_label": { + "type": "string" + }, + "legend_info_title": { + "type": "string" + }, + "legend_verified": { + "type": "string" + }, + "legend_unverified": { + "type": "string" + }, + "legend_stale": { + "type": "string" + }, + "legend_failed": { + "type": "string" + }, + "empty": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/package.json b/package.json index 93e16cf480..3612308778 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@iconify-json/svg-spinners": "1.2.4", "@iconify-json/vscode-icons": "1.2.63", "@intlify/shared": "11.4.6", + "@keytrace/claims": "1.4.0", "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935", "@napi-rs/canvas": "1.0.2", "@nuxt/a11y": "1.0.0-alpha.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5a45017dc..1af393aab4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: '@intlify/shared': specifier: 11.4.6 version: 11.4.6 + '@keytrace/claims': + specifier: 1.4.0 + version: 1.4.0 '@lunariajs/core': specifier: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935 version: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935(supports-color@10.2.2) @@ -2304,6 +2307,10 @@ packages: '@jsr/std__path@1.1.6': resolution: {integrity: sha512-H1Cmg6z8jFyIsQbVV+vZB4eOx4k6Qg8lyUM6l7nZysj5MZumg89kb0M1bsPBTGQis3fw6mJFkJELo5+AK6oCFA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.6.tgz} + '@keytrace/claims@1.4.0': + resolution: {integrity: sha512-XeojwXFFnvhPsDI7f99e2KCvk8gDC5ztKxRBplB+Zx5OmfcaBovX+DZJODZsxz3b1r4ZITBDOjZD8gftgpAZyw==} + engines: {node: '>=18'} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -12920,6 +12927,8 @@ snapshots: dependencies: '@jsr/std__internal': 1.0.14 + '@keytrace/claims@1.4.0': {} + '@kwsites/file-exists@1.1.1(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) diff --git a/server/api/keytrace/[domain].ts b/server/api/keytrace/[domain].ts new file mode 100644 index 0000000000..0ea5c494e2 --- /dev/null +++ b/server/api/keytrace/[domain].ts @@ -0,0 +1,178 @@ +import { getClaimsForHandle, type ClaimVerificationResult } from '@keytrace/claims' +import type { KeytraceProofMethod, KeytraceResponse } from '#shared/types/keytrace' +import { + getOptionalClaimField, + mapKeytraceVerificationStatus, + mapClaimTypeToPlatform, +} from '#server/utils/keytrace' +import { toProxiedImageUrl } from '#server/utils/image-proxy' +import { getSafeHttpUrl } from '#shared/utils/url' + +function domainToDisplayName(domain: string): string { + const firstSegment = domain.split('.')[0] || domain + return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1) +} + +function buildDefaultAvatarUrl(domain: string, imageProxySecret: string): string { + return buildSeededAvatarUrl(domain, imageProxySecret) +} + +function buildSeededAvatarUrl(seed: string, imageProxySecret: string): string { + const rawFallbackAvatar = `https://api.dicebear.com/9.x/initials/svg?seed=${encodeURIComponent(seed)}` + return toProxiedImageUrl(rawFallbackAvatar, imageProxySecret) +} + +function toProfileAvatarUrl( + rawAvatarUrl: string | undefined, + domain: string, + imageProxySecret: string, +): string { + const safeAvatarUrl = getSafeHttpUrl(rawAvatarUrl) + if (!safeAvatarUrl) { + return buildDefaultAvatarUrl(domain, imageProxySecret) + } + + return toProxiedImageUrl(safeAvatarUrl, imageProxySecret) +} + +function toAccountAvatarUrl( + rawAvatarUrl: string | undefined, + imageProxySecret: string, + fallbackSeed: string, +): string { + const safeAvatarUrl = getSafeHttpUrl(rawAvatarUrl) + if (!safeAvatarUrl) { + return buildSeededAvatarUrl(fallbackSeed, imageProxySecret) + } + + return toProxiedImageUrl(safeAvatarUrl, imageProxySecret) +} + +function buildFallbackProfile(domain: string, imageProxySecret: string): KeytraceResponse { + return { + profile: { + name: `${domainToDisplayName(domain)} Developer`, + avatar: buildDefaultAvatarUrl(domain, imageProxySecret), + description: `No Keytrace claims found for ${domain}.`, + }, + accounts: [], + } +} + +function buildServiceUnavailableProfile( + domain: string, + imageProxySecret: string, +): KeytraceResponse { + return { + profile: { + name: `${domainToDisplayName(domain)} Developer`, + avatar: buildDefaultAvatarUrl(domain, imageProxySecret), + description: 'Keytrace is temporarily unavailable. Please try again shortly.', + }, + accounts: [], + } +} + +const allowedProofMethods = new Set