@oxy.so/core 7.9.0 → 7.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/HttpService.js +113 -12
- package/dist/cjs/i18n/compactLocale.js +36 -0
- package/dist/cjs/i18n/compactLocaleValues.generated.js +4 -0
- package/dist/cjs/i18n/index.js +15 -15
- package/dist/cjs/i18n/locales/compact/ar-SA.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/bn-BD.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/ca-ES.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/de-DE.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/es-ES.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/fr-FR.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/hi-IN.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/id-ID.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/it-IT.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/ja-JP.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/ko-KR.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/pt-PT.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/ru-RU.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/tr-TR.generated.js +5 -0
- package/dist/cjs/i18n/locales/compact/zh-CN.generated.js +5 -0
- package/dist/cjs/shared/utils/errorUtils.js +5 -2
- package/dist/cjs/utils/asyncUtils.js +6 -0
- package/dist/cjs/utils/authHelpers.js +3 -0
- package/dist/esm/HttpService.js +113 -12
- package/dist/esm/i18n/compactLocale.js +30 -0
- package/dist/esm/i18n/compactLocaleValues.generated.js +2 -0
- package/dist/esm/i18n/index.js +15 -15
- package/dist/esm/i18n/locales/compact/ar-SA.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/bn-BD.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/ca-ES.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/de-DE.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/es-ES.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/fr-FR.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/hi-IN.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/id-ID.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/it-IT.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/ja-JP.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/ko-KR.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/pt-PT.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/ru-RU.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/tr-TR.generated.js +3 -0
- package/dist/esm/i18n/locales/compact/zh-CN.generated.js +3 -0
- package/dist/esm/shared/utils/errorUtils.js +3 -1
- package/dist/esm/utils/asyncUtils.js +6 -0
- package/dist/esm/utils/authHelpers.js +3 -0
- package/dist/types/i18n/compactLocale.d.ts +2 -0
- package/dist/types/i18n/compactLocaleValues.generated.d.ts +2 -0
- package/dist/types/i18n/locales/compact/ar-SA.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/bn-BD.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/ca-ES.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/de-DE.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/es-ES.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/fr-FR.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/hi-IN.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/id-ID.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/it-IT.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/ja-JP.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/ko-KR.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/pt-PT.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/ru-RU.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/tr-TR.generated.d.ts +3 -0
- package/dist/types/i18n/locales/compact/zh-CN.generated.d.ts +3 -0
- package/dist/types/shared/utils/errorUtils.d.ts +2 -0
- package/package.json +5 -3
- package/src/HttpService.ts +122 -12
- package/src/i18n/README.md +11 -0
- package/src/i18n/compactLocale.ts +32 -0
- package/src/i18n/compactLocaleValues.generated.ts +2 -0
- package/src/i18n/index.ts +15 -15
- package/src/i18n/locales/compact/ar-SA.generated.ts +4 -0
- package/src/i18n/locales/compact/bn-BD.generated.ts +4 -0
- package/src/i18n/locales/compact/ca-ES.generated.ts +4 -0
- package/src/i18n/locales/compact/de-DE.generated.ts +4 -0
- package/src/i18n/locales/compact/es-ES.generated.ts +4 -0
- package/src/i18n/locales/compact/fr-FR.generated.ts +4 -0
- package/src/i18n/locales/compact/hi-IN.generated.ts +4 -0
- package/src/i18n/locales/compact/id-ID.generated.ts +4 -0
- package/src/i18n/locales/compact/it-IT.generated.ts +4 -0
- package/src/i18n/locales/compact/ja-JP.generated.ts +4 -0
- package/src/i18n/locales/compact/ko-KR.generated.ts +4 -0
- package/src/i18n/locales/compact/pt-PT.generated.ts +4 -0
- package/src/i18n/locales/compact/ru-RU.generated.ts +4 -0
- package/src/i18n/locales/compact/tr-TR.generated.ts +4 -0
- package/src/i18n/locales/compact/zh-CN.generated.ts +4 -0
- package/src/shared/utils/errorUtils.ts +5 -1
- package/src/utils/asyncUtils.ts +7 -0
- package/src/utils/authHelpers.ts +2 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Generated by scripts/generateCompactLocales.ts; do not edit.
|
|
2
|
+
import { restoreCompactLocale } from '../../compactLocale.js';
|
|
3
|
+
export default restoreCompactLocale({ "signin": { "title": "Giriş yap", "subtitle": "Oxy hesabınızı kullanın", "addAccountTitle": "Başka bir hesap ekle", "addAccountSubtitle": "Başka bir hesapla giriş yapın", "actions": { "continue": "Devam", "back": "Geri" }, "createAccountLink": "Oxy'de yeni misiniz? Hesap oluşturun", "status": { "signingIn": "Giriş yapılıyor…" }, "subtitleToApp": "{{app}} uygulamasına devam etmek için", "orContinueWith": "veya şununla devam edin", "qr": { "caption": "Giriş yapmak için Commons ile tarayın", "renew": "Yeni kod göster" }, "device": { "codeHint": "Telefonunda bu kodun göründüğünü kontrol et" }, "errors": { "rateLimited": "Çok fazla deneme yapıldı. {{seconds}} sn sonra tekrar deneyin.", "invalidCredentials": "Kullanıcı adı, e-posta veya şifre yanlış. Bu e-postayı birden fazla hesap kullanıyorsa kullanıcı adıyla giriş yapın.", "emailAccountsFull": "Bu e-postaya bağlanabilecek hesap sayısı doldu.", "codeInvalid": "Kod yanlış veya süresi dolmuş.", "requestExpired": "Bu girişin süresi doldu. Yeni bir e-posta gönderin.", "secondFactorInvalid": "Kod yanlış. Güncel kodu deneyin.", "originNotAllowed": "Burada giriş yapılamıyor.", "generic": "Bir sorun oluştu. Lütfen tekrar deneyin.", "notConfigured": "Giriş kullanılamıyor", "notConfiguredDescription": "{{app}} henüz giriş için ayarlanmadı. Uygulamanın geliştiricisiyle iletişime geçin.", "failed": "Giriş başlatılamadı", "failedDescription": "Bir sorun oluştu. Lütfen tekrar deneyin." }, "emailAccounts": { "title": "Bir hesap seçin", "description": "{{identifier}} için birden fazla hesap var. Giriş yapmak istediğiniz hesabı seçin." }, "chooser": { "subtitleToApp": "{{app}} uygulamasına devam etmek için", "title": "Bir hesap seçin", "subtitle": "Aşağıdaki hesaplardan biriyle devam edin veya başka bir hesap kullanın.", "useAnother": "Başka bir hesap kullan", "continueAs": "{{name}} olarak devam et" }, "terms": { "before": "Devam ederek şunları kabul etmiş olursunuz:", "termsLink": "Hizmet Şartları", "and": "ve", "privacyLink": "Gizlilik Politikası" }, "noAccount": "Hesabınız yok mu?", "createAccount": "Hesap oluştur", "identifier": { "label": "E-posta veya kullanıcı adı", "placeholder": "ornek@example.com", "required": "E-postanızı veya kullanıcı adınızı girin." }, "checkEmail": { "title": "E-postanızı kontrol edin", "description": "{{identifier}} ile eşleşen bir hesap varsa ona bir kod ve giriş bağlantısı gönderdik.", "codeLabel": "Kod", "codeHint": "E-postadaki kodu yazın veya e-postadaki bağlantıyı bu tarayıcıda açın.", "codeHasLetters": "Kodunuzda harf var mı?", "codeDigitsOnly": "Kodunuz yalnızca rakamlardan mı oluşuyor?", "resend": "Yeni e-posta gönder", "resendIn": "{{seconds}} sn sonra yeni e-posta gönder", "resent": "Yeni bir e-posta gönderdik.", "retryLater": "Zaten birkaç e-posta gönderdik. Birkaç dakika bekleyip tekrar deneyin.", "usePassword": "Bunun yerine şifrenizi kullanın", "differentAccount": "Farklı bir hesap kullan" }, "password": { "title": "Şifrenizi girin", "label": "Şifre", "required": "Şifrenizi girin.", "forgot": "Unuttunuz mu? E-postayla kod alın" }, "secondFactor": { "title": "İki adımlı doğrulama", "description": "Kimlik doğrulama uygulamanızdaki kodu girin.", "backupDescription": "Yedek kodlarınızdan birini girin. Her kod yalnızca bir kez çalışır.", "label": "Doğrulama kodu", "backupLabel": "Yedek kod", "useBackup": "Yedek kod kullan", "useAuthenticator": "Kimlik doğrulama uygulamanızı kullanın" }, "link": { "approvedTitle": "Giriş yaptınız", "approvedDescription": "Giriş isteğinde bulunduğunuz uygulamaya dönün: işlem kendiliğinden devam eder. Bu sekmeyi kapatabilirsiniz.", "otherDeviceTitle": "Bağlantıyı aynı tarayıcıda açın", "otherDeviceDescription": "Bu bağlantı yalnızca giriş isteğinde bulunduğunuz tarayıcıda çalışır. Bağlantıyı orada açın veya e-postadaki kodu uygulamaya yazın.", "invalidTitle": "Bu bağlantı kullanılamıyor", "invalidDescription": "Süresi dolmuş veya zaten kullanılmış. Uygulamadan yeni bir e-posta isteyin veya en son e-postadaki kodu yazın." } }, "signup": { "title": "Hesabınızı oluşturun", "subtitle": "Kullanıcı adınızı seçin. Ardından e-postanızı doğrulayalım.", "createInCommons": "Bunun yerine Commons'ta oluşturun", "backToSignInLink": "Zaten hesabınız var mı? Giriş yapın", "username": { "label": "Kullanıcı adı", "placeholder": "adiniz", "required": "Bir kullanıcı adı seçin.", "taken": "Bu kullanıcı adı alınmış." }, "email": { "title": "E-postanız nedir?", "subtitle": "Onunla giriş yaparsınız: her seferinde size bir kod göndeririz. Başka kimse göremez.", "label": "E-posta", "placeholder": "ornek@example.com", "invalid": "Geçerli bir e-posta adresi girin." }, "commonsSubtitle": "Ya da kimliğinizi Commons'ta oluşturun ve anahtarınızı kendiniz saklayın.", "laterNote": "Daha sonra hesabınızın güvenlik ayarlarından şifre veya kimlik doğrulama uygulaması ekleyebilirsiniz." }, "accountSwitcher": { "loading": "Hesaplar yükleniyor...", "qrHeadline": "Oxy kimliğinizle giriş yapın", "signInWithOxy": "Oxy ile giriş yap", "signUpWithOxy": "Oxy ile kaydol", "getCommons": "Commons'ı indir", "commonsNotInstalled": "Commons yok mu? Oxy kimliğinizle giriş yapmak için uygulamayı indirin.", "showQrAnyway": "Commons başka bir cihazımda var", "switchWhileSignedInAs": "Hesap değiştir, {{name}} olarak giriş yapıldı", "manageOnDevice": "Bu cihazdaki hesapları yönet", "continueWithOxy": "Oxy ile devam et", "havingTrouble": "Sorun mu yaşıyorsunuz?", "progress": { "preparing": "İstek hazırlanıyor", "awaitingApproval": "Onay bekleniyor", "scanWithCommons": "Telefonunuzdaki Commons ile tarayın", "continueInCommons": "Commons'ta devam edin", "checkCommons": "Telefonunuzdaki Commons'ı kontrol edin", "openedInCommons": "Commons'ta açıldı", "confirming": "Kimlik doğrulanıyor", "confirmed": "Kimlik doğrulandı" }, "signInFailures": { "denied": "Giriş Commons'ta reddedildi.", "expired": "Bu giriş isteğinin süresi doldu. Lütfen tekrar deneyin.", "network": "Oxy'ye ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin.", "notConfigured": "Bu uygulama henüz giriş için ayarlanmadı.", "unsupportedFlow": "Bu giriş burada tamamlanamıyor.", "claimFailed": "Giriş onaylandı ancak tamamlanamadı. Lütfen tekrar deneyin.", "generic": "Giriş başarısız oldu. Lütfen tekrar deneyin." }, "linkOpenFailed": "Bağlantı açılamadı. Lütfen tekrar deneyin." }, "common": { "stepOf": "Adım {{step}}/{{total}}", "actions": { "back": "Geri", "continue": "Devam", "next": "İleri", "getStarted": "Başlayın", "createAccount": "Hesap Oluştur", "signIn": "Giriş Yap", "verify": "Doğrula", "resetPassword": "Şifreyi Sıfırla", "signedOut": "Çıkış yapıldı", "close": "Kapat", "tryAgain": "Tekrar dene" }, "links": { "recoverAccount": "Hesabınızı kurtarın", "signUp": "Kaydol" }, "labels": { "username": "Kullanıcı Adı", "email": "E-posta", "password": "Şifre", "confirmPassword": "Şifreyi Onayla" }, "revoke": "İptal et", "errors": { "signOutAllFailed": "Tüm hesaplardan çıkış yapılırken bir sorun oluştu. Lütfen tekrar deneyin." } }, "notifications": { "title": "Bildirimler", "subtitle": "Anlık, e-posta ve güvenlik bildirimlerini yönetin", "updateError": "Bildirim tercihleri güncellenemedi", "sections": { "channels": "Kanallar", "alerts": "Uyarılar", "marketing": "Pazarlama" }, "items": { "push": { "title": "Anlık bildirimler", "subtitle": "Cihazlarınızda gerçek zamanlı uyarılar" }, "emailDigest": { "title": "E-posta özeti", "subtitle": "Hesap etkinliğinizin dönemsel özeti" }, "securityAlerts": { "title": "Güvenlik uyarıları", "subtitle": "Girişler, kurtarma kodları ve anahtar değişiklikleri" }, "marketingEmails": { "title": "Pazarlama e-postaları", "subtitle": "Ürün haberleri ve ara sıra kampanyalar" } } }, "preferences": { "title": "Tercihler", "subtitle": "Tema, hareket ve bölge ayarları", "sections": { "appearance": "Görünüm", "language": "Dil", "region": "Bölge" }, "theme": { "light": "Açık", "dark": "Koyu", "system": "Sistem varsayılanı" }, "items": { "theme": { "title": "Tema" }, "reduceMotion": { "title": "Hareketi azalt", "subtitle": "Oxy uygulamalarında animasyonları en aza indirin", "systemOn": "Sistem ayarı uygulanıyor: hareketi azaltma açık" }, "language": { "title": "Dil" }, "timezone": { "title": "Saat dilimi", "unknown": "Saat dilimi algılanamadı" }, "about": { "title": "Tercihler hakkında", "subtitle": "Tercihler, giriş yaptığınız tüm Oxy uygulamalarında eşitlenir" } } }, "connectedApps": { "title": "Bağlı uygulamalar", "subtitle": "Üçüncü taraf uygulama erişimini yönetin", "empty": { "title": "Bağlı uygulama yok", "subtitle": "Oxy hesabınızla giriş yapmasına izin verdiğiniz uygulamalar burada görünür" }, "item": { "granted": "{{relative}} izin verildi", "lastUsed": "Son kullanım: {{relative}}" }, "confirm": { "title": "Erişimi iptal et", "message": "{{name}} uygulamasının Oxy hesabınıza erişimi iptal edilsin mi?" }, "toasts": { "revoked": "{{name}} uygulamasının erişimi iptal edildi", "revokeFailed": "Erişim iptal edilemedi" } }, "accountMenu": { "label": "Hesap menüsü", "manage": "Oxy hesabınızı yönetin", "viewProfile": "Profili görüntüle", "addAnother": "Başka bir hesap ekle", "signOutAll": "Tüm hesaplardan çıkış yap", "open": "Hesap menüsü", "openHint": "Hesap menüsünü açar", "openWithUser": "{{name}} için hesap menüsü", "switching": "Hesap değiştiriliyor…", "signOutAccount": "{{name}} hesabından çıkış yap", "greeting": "Merhaba {{name}}!", "switchAccount": "Hesap değiştir", "storage": { "title": "Oxy depolama alanı", "usage": "{{used}} / {{total}} kullanıldı", "used": "Kullanılan", "free": "Boş", "unavailable": "Kullanım ayrıntıları alınamıyor", "upgrade": "Planı yükselt", "manage": "Depolama alanını yönet" }, "data": "Oxy'deki verileriniz", "settings": "Oxy ayarları", "help": "Yardım ve geri bildirim", "signOut": "Çıkış yap", "privacy": "Gizlilik Politikası", "terms": "Hizmet Şartları" }, "emailCode": { "title": "E-postanızı kontrol edin", "sentTo": "{{email}} adresine 6 haneli bir kod gönderdik.", "label": "Kod", "resend": "Yeni kod gönder", "resent": "Yeni bir kod gönderdik.", "changeEmail": "Başka bir e-posta kullan", "errors": { "codeInvalid": "Kod yanlış veya süresi dolmuş.", "tooManyAttempts": "Çok fazla yanlış kod girildi. Yeni bir kod isteyin.", "expired": "Bu işlem çok uzun sürdü. Baştan başlayın.", "unavailable": "Oxy şu anda e-posta gönderemiyor. Daha sonra tekrar deneyin.", "rateLimited": "Çok fazla kod istendi. Daha sonra tekrar deneyin." } }, "deleteAccount": { "keyless": { "subtitle": "@{{username}} ve içindeki her şey kalıcı olarak silinir. Kullanıcı adınızı yazın, ardından size e-postayla göndereceğimiz kodla onaylayın.", "action": "Hesabı sil", "done": "Hesabınız silindi.", "doneDescription": "Oturumunuz kapatıldı.", "keyed": "Bu hesap Commons kullanıyor: hesabın anahtarıyla Oxy Commons'ta Ayarlar > Hesabı sil bölümünden silin." }, "handoff": { "elsewhereMessage": "Hesabınızı silmek için kimlik anahtarınız gerekiyor ve bu anahtar bu cihazda değil. Anahtarın bulunduğu cihazda Oxy Commons'ı açın ve Ayarlar > Hesabı sil bölümüne gidin." } }, "linkCommons": { "title": "Commons'ı bağla", "subtitle": "Anahtarınızı kendiniz saklayın: Commons'ı bağladığınızda bu hesap yalnızca size ait olur. Hesabın e-postası silinir ve hesaba yeniden erişmenin yolu Commons'taki kurtarma ifadeniz olur.", "scan": "Telefonunuzda Commons'ı açın, “Web'de bir Oxy hesabım var” seçeneğini seçin ve bu kodu tarayın.", "waiting": "Commons bekleniyor…", "compareTitle": "Kodu kontrol edin", "compare": "Commons aynı kodu gösterir. Göstermiyorsa iptal edin.", "confirm": "Evet, Commons'ı bağla", "cancel": "İptal", "doneTitle": "Commons bağlandı", "done": "Bu hesabın anahtarı artık sizde. E-postası silindi: hesaba yeniden erişmenin yolu Commons'taki kurtarma ifadeniz.", "already": "Bu hesap zaten Commons kullanıyor.", "expired": "Bu kodun süresi doldu.", "renew": "Yeni kod göster", "failed": "Commons bağlanamadı. Tekrar deneyin.", "confirmTitle": "Bağlantıyı onaylayın", "confirmDescription": "Bitirmek için size e-postayla gönderdiğimiz kodla siz olduğunuzu onaylayın.", "confirmPasswordDescription": "Bitirmek için şifrenizle siz olduğunuzu onaylayın.", "row": "Commons ile anahtarınızı kendiniz saklayın" }, "reauth": { "title": "Siz olduğunuzu onaylayın", "emailDescription": "Bu değişikliği onaylamak için e-postanıza bir kod göndereceğiz.", "sendCode": "Kod gönder", "codeSent": "E-postanıza 6 haneli bir kod gönderdik.", "codeLabel": "E-postadaki kod", "passwordLabel": "Mevcut şifre", "usePassword": "Bunun yerine şifrenizi kullanın", "useEmail": "Bunun yerine e-postayla kod alın", "errors": { "invalid": "Olmadı. Yazdığınızı kontrol edip tekrar deneyin.", "totpRequired": "Kimlik doğrulama uygulamanızdaki kodu da girin." } }, "signInSecurity": { "title": "Giriş ve güvenlik", "subtitle": "E-posta, şifre, kimlik doğrulama uygulaması ve oturum açtığınız yerler", "methodsTitle": "Giriş yöntemleri", "notPersonal": "Yalnızca kişisel hesaplar e-posta, şifre veya kimlik doğrulama uygulamasıyla giriş yapabilir.", "loadFailed": "Bu hesabın giriş yöntemleri yüklenemedi.", "retry": "Tekrar dene", "noEmailWarning": "Bu hesapta e-posta yok. Yalnızca şifrenizle giriş yaparsınız; şifrenizi unutursanız hesabı geri almanın bir yolu yoktur.", "status": { "on": "Açık", "off": "Kapalı" }, "commons": { "title": [1] }, "errors": { "lastSignInMethod": "Bu, tek giriş yönteminiz. Önce başka bir yöntem ekleyin.", "passwordRequired": "Önce bir şifre belirleyin ve onunla onaylayın.", "emailUnchanged": "Bu zaten sizin e-postanız." }, "email": { "title": "E-posta", "none": "E-posta yok", "description": "Giriş kodları, giriş bağlantıları ve güvenlik bildirimleri bu adrese gönderilir.", "noneDescription": "Kod veya bağlantıyla giriş yapmak ve şifrenizi unutursanız hesabınızı geri almak için bir e-posta ekleyin.", "change": "E-postayı değiştir", "add": "E-posta ekle", "remove": "E-postayı kaldır", "removeNeedsPassword": "E-postanızı kaldırmak için önce bir şifre belirleyin: tek giriş yönteminiz o olacak.", "newLabel": "Yeni e-posta adresi", "newDescription": "Size ait olduğunu doğrulamak için yeni adrese bir kod göndereceğiz.", "invalid": "Geçerli bir e-posta adresi girin.", "sendCode": "Kod gönder", "confirmTitle": "Yeni e-postayı onaylayın", "confirmDescription": "{{email}} adresine gönderdiğimiz kodu girin, ardından siz olduğunuzu onaylayın.", "codeLabel": "Yeni e-postaya gönderilen kod", "save": "E-postayı kaydet", "saved": "E-postanız güncellendi. Diğer cihazlarda oturum kapatıldı.", "removeTitle": "E-postanızı kaldırın", "removeDescription": "Yalnızca şifrenizle giriş yapacaksınız. Şifrenizi unutursanız hesabı geri almanın bir yolu yok. Şifrenizle onaylayın.", "removed": "E-postanız kaldırıldı. Diğer cihazlarda oturum kapatıldı." }, "password": { "title": "Şifre", "setTitle": "Şifre belirleyin", "changeTitle": "Şifrenizi değiştirin", "description": "E-postayla gelen kod yerine şifrenizle giriş yapın. En az {{min}} karakter kullanın.", "newLabel": "Yeni şifre", "repeatLabel": "Şifreyi tekrarlayın", "tooShort": "En az {{min}} karakter kullanın.", "mismatch": "Şifreler eşleşmiyor.", "signOutOthers": "Diğer tüm cihazlarda oturumu kapat", "save": "Şifreyi kaydet", "saved": "Şifreniz kaydedildi.", "row": "İsteğe bağlı: şifreyle giriş yapın", "remove": "Şifreyi kaldır", "removeTitle": "Şifrenizi kaldırın", "removeDescription": "E-postanıza gönderilen kod veya bağlantıyla giriş yapacaksınız.", "removed": "Şifreniz kaldırıldı. Diğer cihazlarda oturum kapatıldı." }, "totp": { "title": "Kimlik doğrulama uygulaması", "description": "Oxy, e-posta kodunuzdan veya şifrenizden sonra Google Authenticator ya da 1Password gibi bir uygulamadan da kod ister.", "setUp": "Kur", "scan": "Bu QR kodunu kimlik doğrulama uygulamanızla tarayın veya kurulum anahtarını girin.", "secretLabel": "Kurulum anahtarı", "codeLabel": "Uygulamadaki kod", "enable": "Aç", "enabled": "Kimlik doğrulama uygulaması açık.", "disable": "Kimlik doğrulama uygulamasını kapat", "disabled": "Kimlik doğrulama uygulaması kapalı.", "on": "Açık", "off": "Kapalı", "backupTitle": "Yedek kodlarınızı kaydedin", "backupDescription": "Telefonunuzu kaybederseniz her kodla bir kez giriş yapabilirsiniz. Kodları güvenli bir yerde saklayın: tekrar gösterilmeyecekler.", "copy": "Kodları kopyala", "copied": "Yedek kodlar kopyalandı.", "savedThem": "Kaydettim", "regenerate": "Yeni yedek kodlar al", "regenerateDescription": "Eski yedek kodlarınız artık çalışmaz.", "remaining": "{{count}} yedek kod kaldı", "row": "Giriş yaparken ikinci bir adım", "onWithCodes": "Açık · {{count}} yedek kod kaldı" } }, "fileManagement": { "deleteFile": "Dosyayı Sil", "confirm": "Onayla", "retry": "Tekrar dene", "loadError": { "description": "Bir sorun oluştu. Bağlantınızı kontrol edip tekrar deneyin." }, "moveToTrash": "Çöp kutusuna taşı", "moveToTrashCount": "Çöp kutusuna taşı ({{count}})", "appAccess": { "title": "Erişimi olan uygulamalar", "description": "Bu uygulamalar bu dosyayı kendi başlarına açabilir. Bu erişimi kaldırmak için uygulamanın iznini iptal edin.", "empty": "Hiçbir uygulama bu dosyayı kendi başına açamıyor.", "loadError": "Bu dosyayı açabilen uygulamalar yüklenemedi", "via": { "upload": "Bu dosyayı sizin için depoladı", "link": "Siz paylaştınız" }, "revoke": "İptal et", "confirm": { "title": "{{name}} için erişim iptal edilsin mi?", "message": "{{name}} artık bu dosyayı açamayacak ve başlattığı oynatmalar hemen duracak." }, "toasts": { "revoked": "{{name}} artık bu dosyayı açamaz", "revokeFailed": "{{name}} için erişim iptal edilemedi" }, "a11y": { "revoke": "{{name}} için bu dosyaya erişimi iptal et" } }, "trashView": { "title": "Çöp kutusu", "subtitle": "Dosyaları geri yükleyin veya kalıcı olarak silin", "notice": "Dosyalar siz geri yükleyene veya silene kadar Çöp kutusunda kalır ve depolama alanınızda yer kaplamaya devam eder.", "emptyTitle": "Çöp kutusu boş", "emptyDescription": "Çöp kutusuna taşıdığınız dosyalar burada görünür. Bunları geri yükleyebilir veya kalıcı olarak silebilirsiniz.", "trashedOn": "{{date}} tarihinde çöp kutusuna taşındı", "deleteForever": "Kalıcı olarak sil", "loadError": "Çöp kutunuz yüklenemedi", "a11y": { "restore": "{{name}} öğesini geri yükle", "deleteForever": "{{name}} öğesini kalıcı olarak sil" } }, "a11y": { "moveToTrash": "{{name}} öğesini çöp kutusuna taşı", "deleteFile": "{{name}} öğesini sil", "openTrash": "Çöp kutusunu aç" }, "toasts": { "movedToTrash": "Çöp kutusuna taşındı. Çöp kutusundan geri yükleyebilirsiniz.", "bulkMovedToTrash": "{{count}} dosya çöp kutusuna taşındı", "moveToTrashFailed": "Dosya çöp kutusuna taşınamadı", "restored": "Dosya geri yüklendi", "notInTrash": "Bu dosya artık çöp kutusunda değil.", "restoreFailed": "Dosya geri yüklenemedi", "deletedForever": "Dosya kalıcı olarak silindi", "deleteSuccess": "Dosya başarıyla silindi", "deleteFailed": "Dosya silinemedi", "fileNotFound": "Dosya bulunamadı. Zaten silinmiş olabilir.", "noPermission": "Bu dosyayı silme izniniz yok.", "bulkDeleteSuccess": "{{count}} dosya başarıyla silindi", "bulkDeleteFailed": "{{count}} dosya silinemedi", "bulkDeleteError": "Dosyalar silinemedi" }, "confirms": { "deleteFile": "\"{{filename}}\" dosyasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "moveToTrash": "\"{{filename}}\" çöp kutusuna taşınsın mı? Artık hiçbir yerde gösterilmez ve Çöp kutusundan geri yükleyebilirsiniz.", "moveFilesToTrash": "{{count}} dosya çöp kutusuna taşınsın mı? Artık hiçbir yerde gösterilmezler ve Çöp kutusundan geri yükleyebilirsiniz.", "deleteForever": "\"{{filename}}\" kalıcı olarak silinsin mi? Bu işlem geri alınamaz.", "deleteForeverInUse": "\"{{filename}}\" uygulamalarınızda hâlâ {{count}} yerde kullanılıyor. Silerseniz oradan da kaldırılır. Kalıcı olarak silinsin mi?" } }, "follow": { "pending": "İstek gönderildi", "disabled": "Burada kapalı", "named": "{{label}}: {{name}}", "verbs": { "follow": { "idle": "Takip et", "active": "Takip ediliyor", "everywhere": "Her yerde takibi bırak", "undo": "Bu hesabı takip etmeyi bırakır", "undoNamed": "{{name}} için takibi bırakır", "checking": "Takip durumu kontrol ediliyor", "checkingNamed": "{{name}} için takip durumu kontrol ediliyor" }, "subscribe": { "idle": "Abone ol", "active": "Abone olundu", "everywhere": "Her yerde aboneliği iptal et", "undo": "Aboneliği iptal eder", "undoNamed": "{{name}} aboneliğini iptal eder", "checking": "Abonelik durumu kontrol ediliyor", "checkingNamed": "{{name}} için abonelik durumu kontrol ediliyor" }, "join": { "idle": "Katıl", "active": "Katıldınız", "everywhere": "Her yerde ayrıl", "undo": "Ayrılır", "undoNamed": "{{name}} üyeliğini bırakır", "checking": "Üyelik kontrol ediliyor", "checkingNamed": "{{name}} için üyelik durumu kontrol ediliyor" }, "watch": { "idle": "İzle", "active": "İzleniyor", "everywhere": "Her yerde izlemeyi bırak", "undo": "İzlemeyi bırakır", "undoNamed": "{{name}} için izlemeyi bırakır", "checking": "İzleme durumu kontrol ediliyor", "checkingNamed": "{{name}} için izleme durumu kontrol ediliyor" } }, "all": { "idle": "Tümünü takip et", "hint": "{{count}} hesabın tümünü takip eder", "hintOne": "{{count}} hesabı takip eder", "undoHint": "{{count}} hesabın tümünü takip etmeyi bırakır", "undoHintOne": "{{count}} hesabı takip etmeyi bırakır" }, "menu": { "forDuration": "{{duration}} boyunca {{label}}", "showIn": "{{app}} içinde göster", "hideIn": "{{app}} içinde gösterme", "thisApp": "bu uygulama", "options": "{{label}} seçenekleri", "optionsHint": "Daha fazla seçenek açar" }, "durations": { "day": "24 saat", "threeDays": "72 saat", "week": "bir hafta" }, "errors": { "update": "Takip durumu güncellenemedi", "read": "Takip durumu kontrol edilemedi", "mode": "Bu takip için uygulamanın ayarı değiştirilemedi", "offline": "Çevrimdışısınız. Bağlantınızı kontrol edip tekrar deneyin.", "signedOut": "Devam etmek için tekrar giriş yapın", "forbidden": "Bu uygulamanın bunu yapma izni yok", "notFound": "Bu artık kullanılamıyor", "rateLimited": "Çok fazla deneme yapıldı. Biraz sonra tekrar deneyin." } }, "accounts": { "accountCategory": { "news": "Haberler", "politics": "Siyaset ve politika", "business": "İş dünyası", "startup": "Girişimler ve girişimcilik", "finance": "Finans ve yatırım", "crypto": "Kripto ve web3", "marketplace": "Pazar yeri ve ilanlar", "retail": "Perakende ve e-ticaret", "real_estate": "Gayrimenkul", "agency": "Emlak ofisi", "landlord": "Ev sahipliği ve mülk yönetimi", "cooperative": "Konut kooperatifi", "architecture": "Mimarlık ve inşaat", "technology": "Teknoloji", "software": "Yazılım ve geliştirme", "ai": "Yapay zekâ ve makine öğrenimi", "security": "Güvenlik ve gizlilik", "automation": "Otomasyon ve araçlar", "science": "Bilim ve araştırma", "education": "Eğitim ve öğretim", "books": "Kitaplar ve yazarlık", "health": "Sağlık ve tıp", "fitness": "Fitness ve sağlıklı yaşam", "sports": "Spor", "gaming": "Oyun ve e-spor", "music": "Müzik", "film": "Film ve TV", "podcast": "Podcast ve ses", "art": "Sanat ve tasarım", "photography": "Fotoğrafçılık", "comedy": "Komedi", "food": "Yeme ve içme", "travel": "Seyahat ve mekânlar", "fashion": "Moda ve güzellik", "home_garden": "Ev ve bahçe", "diy": "Kendin yap ve üretim", "automotive": "Otomobil ve sürüş", "animals": "Hayvanlar ve evcil hayvanlar", "family": "Aile ve ebeveynlik", "nonprofit": "Kâr amacı gütmeyen ve yardım kuruluşları", "government": "Devlet ve kamu sektörü", "community": "Yerel topluluk", "activism": "Aktivizm ve savunuculuk", "environment": "Çevre ve sürdürülebilirlik", "religion": "Din ve maneviyat", "other": "Diğer" }, "members": { "title": "Üyeler" }, "settings": { "title": "Hesap ayarları", "subtitle": "Bu hesabın profilini, üyelerini ve erişimini yönetin.", "save": "Değişiklikleri kaydet", "displayName": { "label": "Görünen ad", "invalidChars": "Yalnızca harf, boşluk, kesme işareti ve ad ayırıcıları kullanın", "invalidTitle": "< veya > içermeyen düz metin kullanın (en fazla 300 karakter)" }, "bio": { "label": "Biyografi (isteğe bağlı)" }, "picture": { "change": "Resmi değiştir" }, "categories": { "title": "Kategoriler", "footer": "İlk kategori, profilde gösterilen ana kategoridir. En fazla {{max}}.", "primary": "Ana", "makePrimary": "Ana yap", "remove": "Kaldır: {{category}}", "add": "Kategori ekle", "atCap": "En fazla {{max}}. Başka bir tane eklemek için birini kaldırın.", "unavailable": "Kullanılamayan kategori" }, "sections": { "access": "Erişim", "dangerZone": "Tehlikeli bölge" }, "members": { "subtitle": "Kişileri davet edin ve rolleri yönetin" }, "archive": { "title": "Hesabı arşivle", "subtitle": "Bu hesabı devre dışı bırakın", "confirmTitle": "Hesabı arşivle", "confirmDescription": "Bu hesap arşivlensin mi? Hesap devre dışı bırakılacak ve üyeleri erişimini kaybedecek." }, "errors": { "missingAccount": "Hesap seçilmedi." }, "toasts": { "saved": "Hesap güncellendi", "saveFailed": "Hesap güncellenemedi", "archived": "Hesap arşivlendi", "archiveFailed": "Hesap arşivlenemedi" } } } });
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Generated by scripts/generateCompactLocales.ts; do not edit.
|
|
2
|
+
import { restoreCompactLocale } from '../../compactLocale.js';
|
|
3
|
+
export default restoreCompactLocale({ "signin": { "title": "登录", "subtitle": "使用你的 Oxy 账号", "addAccountTitle": "添加其他账号", "addAccountSubtitle": "使用另一个账户登录", "actions": { "continue": "继续", "back": "返回" }, "createAccountLink": "初次使用 Oxy?创建账户", "status": { "signingIn": "正在登录…" }, "subtitleToApp": "以继续前往 {{app}}", "orContinueWith": "或使用以下方式继续", "qr": { "caption": "使用 Commons 扫码登录", "renew": "显示新的二维码" }, "device": { "codeHint": "请确认手机上显示的是这个代码" }, "errors": { "rateLimited": "尝试次数过多。请在 {{seconds}} 秒后重试。", "invalidCredentials": "用户名、邮箱或密码不正确。 如果多个账号使用此邮箱,请用用户名登录。", "emailAccountsFull": "此邮箱的账号数量已达上限。", "codeInvalid": "验证码不正确或已过期。", "requestExpired": "此次登录已过期。请重新发送邮件。", "secondFactorInvalid": "验证码不正确。请使用当前的验证码。", "originNotAllowed": "此处无法登录。", "generic": "出了点问题,请重试。", "notConfigured": "无法登录", "notConfiguredDescription": "{{app}} 尚未配置登录功能。请联系该应用的开发者。", "failed": "无法开始登录", "failedDescription": "出了点问题,请重试。" }, "emailAccounts": { "title": "选择账号", "description": "{{identifier}} 有多个账号。请选择要登录的账号。" }, "chooser": { "subtitleToApp": "以继续前往 {{app}}", "title": "选择账户", "subtitle": "使用下方的账户继续,或使用其他账户。", "useAnother": "使用其他账户", "continueAs": "以 {{name}} 的身份继续" }, "terms": { "before": "继续即表示你同意我们的", "termsLink": "服务条款", "and": "和", "privacyLink": "隐私政策" }, "noAccount": "还没有账号?", "createAccount": "创建账号", "identifier": { "label": "邮箱或用户名", "placeholder": [2], "required": "请输入你的邮箱或用户名。" }, "checkEmail": { "title": "查看你的邮箱", "description": "如果有与 {{identifier}} 匹配的账户,我们已向其发送验证码和登录链接。", "codeLabel": "验证码", "codeHint": "输入邮件中的验证码,或在此浏览器中打开邮件里的链接。", "codeHasLetters": "验证码里有字母吗?", "codeDigitsOnly": "验证码只有数字吗?", "resend": "重新发送邮件", "resendIn": "{{seconds}} 秒后可重新发送邮件", "resent": "我们已发送新邮件。", "retryLater": "我们已发送了多封邮件。请等几分钟后再试。", "usePassword": "改用密码", "differentAccount": "使用其他账户" }, "password": { "title": "输入你的密码", "label": "密码", "required": "请输入你的密码。", "forgot": "忘记密码?通过邮件获取验证码" }, "secondFactor": { "title": "两步验证", "description": "输入身份验证器应用中的验证码。", "backupDescription": "输入一个备用验证码。每个只能使用一次。", "label": "身份验证器验证码", "backupLabel": "备用验证码", "useBackup": "使用备用验证码", "useAuthenticator": "使用身份验证器应用" }, "link": { "approvedTitle": "你已登录", "approvedDescription": "返回你请求登录的应用,它会自动继续。你可以关闭此标签页。", "otherDeviceTitle": "请在同一浏览器中打开链接", "otherDeviceDescription": "此链接只能在你请求登录的浏览器中使用。请在那里打开,或在应用中输入邮件里的验证码。", "invalidTitle": "此链接无法使用", "invalidDescription": "链接已过期或已被使用。请在应用中重新获取邮件,或输入最新一封邮件中的验证码。" } }, "signup": { "title": "创建你的账户", "subtitle": "选择你的用户名,然后我们确认你的邮箱。", "createInCommons": "改为在 Commons 中创建", "backToSignInLink": "已有账户?登录", "username": { "label": "用户名", "placeholder": [43], "required": "请选择用户名。", "taken": "该用户名已被占用。" }, "email": { "title": "你的邮箱是什么?", "subtitle": "你将用它登录:每次我们都会发送一个验证码。其他人看不到它。", "label": "邮箱", "placeholder": [2], "invalid": "请输入有效的邮箱地址。" }, "commonsSubtitle": "或者在 Commons 中创建你的身份,自己保管密钥。", "laterNote": "你之后可以在账户的安全设置中添加密码或身份验证器应用。" }, "accountSwitcher": { "loading": "正在加载账户...", "qrHeadline": "使用您的 Oxy 身份登录", "signInWithOxy": "使用 Oxy 登录", "signUpWithOxy": "使用 Oxy 注册", "getCommons": "获取 Commons", "commonsNotInstalled": "没有 Commons?获取应用以使用您的 Oxy ID 登录。", "showQrAnyway": "我在另一台设备上有 Commons", "switchWhileSignedInAs": "切换账号,当前登录为 {{name}}", "manageOnDevice": "管理此设备上的账号", "continueWithOxy": "使用 Oxy 继续", "havingTrouble": "遇到问题?", "progress": { "preparing": "正在准备请求", "awaitingApproval": "正在等待批准", "scanWithCommons": "请用手机上的 Commons 扫描", "continueInCommons": "请在 Commons 中继续", "checkCommons": "请查看手机上的 Commons", "openedInCommons": "已在 Commons 中打开", "confirming": "正在确认身份", "confirmed": "身份已确认" }, "signInFailures": { "denied": "登录请求已在 Commons 中被拒绝。", "expired": "登录请求已过期,请重试。", "network": "无法连接到 Oxy。请检查网络连接后重试。", "notConfigured": "此应用尚未设置登录。", "unsupportedFlow": "此登录无法在这里完成。", "claimFailed": "登录已获批准,但未能完成。请重试。", "generic": "登录失败,请重试。" }, "linkOpenFailed": "无法打开链接,请重试。" }, "common": { "stepOf": "第 {{step}} 步,共 {{total}} 步", "actions": { "back": "返回", "continue": "继续", "next": "下一步", "getStarted": "开始", "createAccount": "创建账户", "signIn": "登录", "verify": "验证", "resetPassword": "重置密码", "signedOut": "已退出", "close": "关闭", "tryAgain": "重试" }, "links": { "recoverAccount": "恢复您的账户", "signUp": "注册" }, "labels": { "username": "用户名", "email": "电子邮件", "password": "密码", "confirmPassword": "确认密码" }, "revoke": [32], "errors": { "signOutAllFailed": "退出所有账户时出现问题。请重试。" } }, "notifications": { "title": [23], "subtitle": [18], "updateError": [14], "sections": { "channels": [9], "alerts": [6], "marketing": [4] }, "items": { "push": { "title": [28], "subtitle": [29] }, "emailDigest": { "title": [12], "subtitle": [24] }, "securityAlerts": { "title": [36], "subtitle": [37] }, "marketingEmails": { "title": [20], "subtitle": [27] } } }, "preferences": { "title": [25], "subtitle": [40], "sections": { "appearance": [7], "language": [0], "region": [31] }, "theme": { "light": [17], "dark": [11], "system": [38] }, "items": { "theme": { "title": [39] }, "reduceMotion": { "title": [30], "subtitle": [21], "systemOn": [15] }, "language": { "title": [0] }, "timezone": { "title": [41], "unknown": [42] }, "about": { "title": [5], "subtitle": [26] } } }, "connectedApps": { "title": [10], "subtitle": [19], "empty": { "title": [22], "subtitle": [8] }, "item": { "granted": "已于 {{relative}} 授权", "lastUsed": [16] }, "confirm": { "title": [33], "message": [34] }, "toasts": { "revoked": [35], "revokeFailed": [13] } }, "accountMenu": { "label": "账户菜单", "manage": "管理您的 Oxy 账户", "viewProfile": "查看个人资料", "addAnother": "添加其他账户", "signOutAll": "退出所有账户", "open": "账户菜单", "openHint": "打开账户菜单", "openWithUser": "{{name}} 的账户菜单", "switching": "正在切换账户…", "signOutAccount": "退出 {{name}}", "greeting": "你好,{{name}}!", "switchAccount": "切换账号", "storage": { "title": "Oxy 存储", "usage": "已使用 {{used}} / {{total}}", "used": "已用", "free": "可用", "unavailable": "无法获取用量详情", "upgrade": "升级方案", "manage": "管理存储" }, "data": "你在 Oxy 中的数据", "settings": "Oxy 设置", "help": "帮助与反馈", "signOut": "退出登录", "privacy": "隐私政策", "terms": "服务条款" }, "emailCode": { "title": "查看你的邮箱", "sentTo": "我们已向 {{email}} 发送了 6 位验证码。", "label": "验证码", "resend": "发送新验证码", "resent": "我们已发送新验证码。", "changeEmail": "使用其他邮箱", "errors": { "codeInvalid": "验证码不正确或已过期。", "tooManyAttempts": "错误次数过多,请重新获取验证码。", "expired": "耗时过长,请重新开始。", "unavailable": "Oxy 暂时无法发送邮件,请稍后再试。", "rateLimited": "验证码请求过多,请稍后再试。" } }, "deleteAccount": { "keyless": { "subtitle": "@{{username}} 及其中的一切将被永久删除。请输入你的用户名,然后用我们发到你邮箱的验证码确认。", "action": "删除账户", "done": "你的账户已删除。", "doneDescription": "你已退出登录。", "keyed": "此账户使用 Commons:请在 Oxy Commons 的“设置 > 删除账户”中用其密钥删除。" }, "handoff": { "elsewhereMessage": "删除账户需要你的身份密钥,而它不在此设备上。请在保存密钥的设备上打开 Oxy Commons,前往“设置 > 删除账户”。" } }, "linkCommons": { "title": "关联 Commons", "subtitle": "自己保管密钥:关联 Commons 后,此账户只属于你。它的邮箱会被删除,今后用 Commons 中的恢复短语找回账户。", "scan": "在手机上打开 Commons,选择“我在网页上有 Oxy 账户”,然后扫描此码。", "waiting": "正在等待 Commons…", "compareTitle": "核对代码", "compare": "Commons 会显示相同的代码。如果不同,请取消。", "confirm": "是的,关联 Commons", "cancel": "取消", "doneTitle": "Commons 已关联", "done": "此账户现在由你自己保管。它的邮箱已删除:今后用 Commons 中的恢复短语找回账户。", "already": "此账户已在使用 Commons。", "expired": "此代码已过期。", "renew": "显示新代码", "failed": "无法关联 Commons,请重试。", "confirmTitle": "确认关联", "confirmDescription": "最后,请用我们发到你邮箱的验证码确认是你本人。", "confirmPasswordDescription": "最后,请用你的密码确认是你本人。", "row": "用 Commons 自己保管密钥" }, "reauth": { "title": "确认是你本人", "emailDescription": "我们会向你的邮箱发送验证码,以确认此项更改。", "sendCode": "发送验证码", "codeSent": "我们已向你的邮箱发送了 6 位验证码。", "codeLabel": "邮件中的验证码", "passwordLabel": "当前密码", "usePassword": "改用密码", "useEmail": "改为通过邮件获取验证码", "errors": { "invalid": "验证失败。请检查输入内容后重试。", "totpRequired": "还需输入身份验证器应用中的验证码。" } }, "signInSecurity": { "title": "登录与安全", "subtitle": "邮箱、密码、身份验证器应用以及你的登录位置", "methodsTitle": "登录方式", "notPersonal": "只有个人账户可以使用邮箱、密码或身份验证器应用登录。", "loadFailed": "无法加载此账户的登录方式。", "retry": "重试", "noEmailWarning": "此账户没有邮箱。你只能用密码登录,如果忘记密码,将无法找回账户。", "status": { "on": "已开启", "off": "已关闭" }, "commons": { "title": [1] }, "errors": { "lastSignInMethod": "这是你唯一的登录方式。请先添加另一种方式。", "passwordRequired": "请先设置密码,并用它进行确认。", "emailUnchanged": "这已经是你的邮箱。" }, "email": { "title": "邮箱", "none": "没有邮箱", "description": "登录验证码、登录链接和安全通知都会发送到此地址。", "noneDescription": "添加邮箱后,你可以用验证码或链接登录,并在忘记密码时找回账户。", "change": "更改邮箱", "add": "添加邮箱", "remove": "移除邮箱", "removeNeedsPassword": "要移除邮箱,请先设置密码:它将成为你唯一的登录方式。", "newLabel": "新邮箱地址", "newDescription": "我们会向新地址发送验证码,以确认它属于你。", "invalid": "请输入有效的邮箱地址。", "sendCode": "发送验证码", "confirmTitle": "确认新邮箱", "confirmDescription": "输入我们发送到 {{email}} 的验证码,然后确认是你本人。", "codeLabel": "发送到新邮箱的验证码", "save": "保存邮箱", "saved": "你的邮箱已更新。其他设备已退出登录。", "removeTitle": "移除你的邮箱", "removeDescription": "你将只能用密码登录。如果忘记密码,将无法找回账户。请用密码确认。", "removed": "你的邮箱已移除。其他设备已退出登录。" }, "password": { "title": "密码", "setTitle": "设置密码", "changeTitle": "更改密码", "description": "用密码登录,而不是通过邮件验证码。至少使用 {{min}} 个字符。", "newLabel": "新密码", "repeatLabel": "再次输入密码", "tooShort": "至少使用 {{min}} 个字符。", "mismatch": "两次输入的密码不一致。", "signOutOthers": "在其他所有地方退出登录", "save": "保存密码", "saved": "你的密码已保存。", "row": "可选:使用密码登录", "remove": "移除密码", "removeTitle": "移除你的密码", "removeDescription": "你将使用发送到邮箱的验证码或链接登录。", "removed": "你的密码已移除。其他设备已退出登录。" }, "totp": { "title": "身份验证器应用", "description": "在邮件验证码或密码之后,Oxy 还会要求输入来自 Google Authenticator 或 1Password 等应用的验证码。", "setUp": "设置", "scan": "用你的身份验证器应用扫描此二维码,或输入设置密钥。", "secretLabel": "设置密钥", "codeLabel": "应用中的验证码", "enable": "开启", "enabled": "身份验证器应用已开启。", "disable": "关闭身份验证器应用", "disabled": "身份验证器应用已关闭。", "on": "已开启", "off": "已关闭", "backupTitle": "保存你的备用验证码", "backupDescription": "如果你丢失了手机,每个验证码可让你登录一次。请妥善保管:它们不会再次显示。", "copy": "复制验证码", "copied": "备用验证码已复制。", "savedThem": "我已保存", "regenerate": "获取新的备用验证码", "regenerateDescription": "你以前的备用验证码将失效。", "remaining": "剩余 {{count}} 个备用验证码", "row": "登录时的第二步验证", "onWithCodes": "已开启 · 剩余 {{count}} 个备用验证码" } }, "fileManagement": { "deleteFile": "删除文件", "confirm": "确认", "retry": "重试", "loadError": { "description": "出了点问题。请检查网络连接后重试。" }, "moveToTrash": "移到回收站", "moveToTrashCount": "移到回收站({{count}})", "appAccess": { "title": "有访问权限的应用", "description": "这些应用可以自行打开此文件。撤销某个应用的访问权限即可收回。", "empty": "没有应用可以自行打开此文件。", "loadError": "无法加载可以打开此文件的应用", "via": { "upload": "为你保存了此文件", "link": "由你分享" }, "revoke": "撤销", "confirm": { "title": "撤销 {{name}} 的访问权限?", "message": "{{name}} 将无法再打开此文件,它启动的任何播放也会立即停止。" }, "toasts": { "revoked": "{{name}} 已无法打开此文件", "revokeFailed": "无法撤销 {{name}} 的访问权限" }, "a11y": { "revoke": "撤销 {{name}} 对此文件的访问权限" } }, "trashView": { "title": "回收站", "subtitle": "恢复文件或将其永久删除", "notice": "文件会一直留在回收站中,直到你恢复或删除它们,并且仍会占用你的存储空间。", "emptyTitle": "回收站是空的", "emptyDescription": "你移到回收站的文件会显示在这里。你可以恢复它们,或将其永久删除。", "trashedOn": "于 {{date}} 移到回收站", "deleteForever": "永久删除", "loadError": "无法加载回收站", "a11y": { "restore": "恢复 {{name}}", "deleteForever": "永久删除 {{name}}" } }, "a11y": { "moveToTrash": "将 {{name}} 移到回收站", "deleteFile": "删除 {{name}}", "openTrash": "打开回收站" }, "toasts": { "movedToTrash": "已移到回收站。你可以从回收站恢复它。", "bulkMovedToTrash": "已将 {{count}} 个文件移到回收站", "moveToTrashFailed": "无法将文件移到回收站", "restored": "文件已恢复", "notInTrash": "此文件已不在回收站中。", "restoreFailed": "无法恢复文件", "deletedForever": "文件已永久删除", "deleteSuccess": "文件已删除", "deleteFailed": "无法删除文件", "fileNotFound": "找不到文件。它可能已被删除。", "noPermission": "你没有删除此文件的权限。", "bulkDeleteSuccess": "已删除 {{count}} 个文件", "bulkDeleteFailed": "{{count}} 个文件删除失败", "bulkDeleteError": "无法删除文件" }, "confirms": { "deleteFile": "确定要删除“{{filename}}”吗?此操作无法撤销。", "moveToTrash": "将“{{filename}}”移到回收站?它将不再在任何地方显示,你可以从回收站恢复它。", "moveFilesToTrash": "将 {{count}} 个文件移到回收站?它们将不再在任何地方显示,你可以从回收站恢复它们。", "deleteForever": "永久删除“{{filename}}”?此操作无法撤销。", "deleteForeverInUse": "“{{filename}}”仍在你的应用中的 {{count}} 处使用。删除后,它也会从那里消失。要永久删除吗?" } }, "follow": { "pending": "已请求", "disabled": "此处已关闭", "named": "{{label}}:{{name}}", "verbs": { "follow": { "idle": "关注", "active": "已关注", "everywhere": "在所有地方取消关注", "undo": "取消关注此账号", "undoNamed": "取消关注 {{name}}", "checking": "正在检查关注状态", "checkingNamed": "正在检查你是否关注了 {{name}}" }, "subscribe": { "idle": "订阅", "active": "已订阅", "everywhere": "在所有地方取消订阅", "undo": "取消订阅", "undoNamed": "取消订阅 {{name}}", "checking": "正在检查订阅状态", "checkingNamed": "正在检查你是否订阅了 {{name}}" }, "join": { "idle": "加入", "active": "已加入", "everywhere": "在所有地方退出", "undo": "退出", "undoNamed": "退出 {{name}}", "checking": "正在检查成员身份", "checkingNamed": "正在检查你是否是 {{name}} 的成员" }, "watch": { "idle": "追踪", "active": "追踪中", "everywhere": "在所有地方停止追踪", "undo": "停止追踪", "undoNamed": "停止追踪 {{name}}", "checking": "正在检查追踪状态", "checkingNamed": "正在检查你是否追踪了 {{name}}" } }, "all": { "idle": "全部关注", "hint": "关注全部 {{count}} 个账号", "hintOne": "关注 {{count}} 个账号", "undoHint": "取消关注全部 {{count}} 个账号", "undoHintOne": "取消关注 {{count}} 个账号" }, "menu": { "forDuration": "{{label}} {{duration}}", "showIn": "在{{app}}中显示", "hideIn": "不在{{app}}中显示", "thisApp": "此应用", "options": "{{label}}选项", "optionsHint": "打开更多选项" }, "durations": { "day": "24 小时", "threeDays": "72 小时", "week": "一周" }, "errors": { "update": "无法更新状态", "read": "无法检查状态", "mode": "无法更改此应用的设置", "offline": "你已离线。请检查网络连接后重试。", "signedOut": "请重新登录以继续", "forbidden": "此应用无权执行此操作", "notFound": "此内容已不可用", "rateLimited": "尝试次数过多,请稍后再试。" } }, "accounts": { "accountCategory": { "news": "新闻", "politics": "政治与政策", "business": "商业", "startup": "创业", "finance": "金融与投资", "crypto": "加密货币与 Web3", "marketplace": "市集与分类信息", "retail": "零售与电商", "real_estate": "房地产", "agency": "房产中介", "landlord": "房东与物业管理", "cooperative": "住房合作社", "architecture": "建筑与施工", "technology": "科技", "software": "软件与开发", "ai": "AI 与机器学习", "security": "安全与隐私", "automation": "自动化与工具", "science": "科学与研究", "education": "教育与培训", "books": "图书与写作", "health": "健康与医疗", "fitness": "健身与身心健康", "sports": "体育", "gaming": "游戏与电竞", "music": "音乐", "film": "影视", "podcast": "播客与音频", "art": "艺术与设计", "photography": "摄影", "comedy": "喜剧", "food": "美食与饮品", "travel": "旅行与地点", "fashion": "时尚与美妆", "home_garden": "家居与园艺", "diy": "DIY 与手作", "automotive": "汽车", "animals": "动物与宠物", "family": "家庭与育儿", "nonprofit": "非营利与慈善", "government": "政府与公共部门", "community": "本地社区", "activism": "行动主义与倡导", "environment": "环境与可持续发展", "religion": "宗教与灵性", "other": "其他" }, "members": { "title": "成员" }, "settings": { "title": "账户设置", "subtitle": "管理此账户的资料、成员和访问权限。", "save": "保存更改", "displayName": { "label": "显示名称", "invalidChars": "只能使用字母、空格、撇号和名称分隔符", "invalidTitle": "请使用不含 < 或 > 的纯文本(最多 300 个字符)" }, "bio": { "label": "简介(可选)" }, "picture": { "change": "更换图片" }, "categories": { "title": "类别", "footer": "第一个类别是资料中显示的主要类别。最多 {{max}} 个。", "primary": "主要", "makePrimary": "设为主要", "remove": "移除{{category}}", "add": "添加类别", "atCap": "最多 {{max}} 个。移除一个后才能添加其他类别。", "unavailable": "类别不可用" }, "sections": { "access": "访问", "dangerZone": "危险区域" }, "members": { "subtitle": "邀请成员并管理角色" }, "archive": { "title": "归档账户", "subtitle": "停用此账户", "confirmTitle": "归档账户", "confirmDescription": "要归档此账户吗?账户将被停用,其成员将失去访问权限。" }, "errors": { "missingAccount": "未选择账户。" }, "toasts": { "saved": "账户已更新", "saveFailed": "无法更新账户", "archived": "账户已归档", "archiveFailed": "无法归档账户" } } } });
|
|
@@ -46,6 +46,8 @@ export const getErrorStatus = (error) => {
|
|
|
46
46
|
return err.statusCode;
|
|
47
47
|
return undefined;
|
|
48
48
|
};
|
|
49
|
+
/** A locally superseded request is not an unauthorized response from the server. */
|
|
50
|
+
export const isSessionChangedError = (error) => !!error && typeof error === 'object' && 'code' in error && error.code === 'AUTH_SESSION_CHANGED';
|
|
49
51
|
/**
|
|
50
52
|
* Extracts the error message from an error object.
|
|
51
53
|
*
|
|
@@ -98,7 +100,7 @@ export const isAlreadyRegisteredError = (error) => {
|
|
|
98
100
|
* @returns true if the error is a 401 Unauthorized
|
|
99
101
|
*/
|
|
100
102
|
export const isUnauthorizedError = (error) => {
|
|
101
|
-
return getErrorStatus(error) === HttpStatus.UNAUTHORIZED;
|
|
103
|
+
return !isSessionChangedError(error) && getErrorStatus(error) === HttpStatus.UNAUTHORIZED;
|
|
102
104
|
};
|
|
103
105
|
/**
|
|
104
106
|
* Check if an error is a forbidden error (HTTP 403).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Retry with exponential backoff and jitter — the policy `HttpService` uses.
|
|
3
3
|
*/
|
|
4
4
|
import { ErrorCodes, isCancelledError } from './errorUtils.js';
|
|
5
|
+
import { isSessionChangedError } from '../shared/utils/errorUtils.js';
|
|
5
6
|
/**
|
|
6
7
|
* Whether an error came from a request hitting its own timeout.
|
|
7
8
|
*
|
|
@@ -99,6 +100,11 @@ export async function retryAsync(operation, maxRetriesOrOptions = 3, baseDelayAr
|
|
|
99
100
|
if (isCancelledError(error)) {
|
|
100
101
|
break;
|
|
101
102
|
}
|
|
103
|
+
// A changed authentication context is a cancellation of this work, even
|
|
104
|
+
// if the caller's custom predicate would retry ordinary 401 responses.
|
|
105
|
+
if (isSessionChangedError(error)) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
102
108
|
if (!retryOnTimeout && isTimeoutError(error)) {
|
|
103
109
|
break;
|
|
104
110
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Authentication helper utilities for common token validation
|
|
3
3
|
* and authentication error handling patterns.
|
|
4
4
|
*/
|
|
5
|
+
import { isSessionChangedError } from '../shared/utils/errorUtils.js';
|
|
5
6
|
/**
|
|
6
7
|
* Error thrown when session sync is required
|
|
7
8
|
*/
|
|
@@ -54,6 +55,8 @@ export async function ensureValidToken(oxyServices, _activeSessionId, syncSessio
|
|
|
54
55
|
* Checks if an error is an authentication error (401 or auth-related message)
|
|
55
56
|
*/
|
|
56
57
|
export function isAuthenticationError(error) {
|
|
58
|
+
if (isSessionChangedError(error))
|
|
59
|
+
return false;
|
|
57
60
|
if (!error || typeof error !== 'object') {
|
|
58
61
|
return false;
|
|
59
62
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: readonly ["Language", "Commons", "you@example.com", "Email", "Marketing", "About preferences", "Alerts", "Appearance", "Apps you authorize to sign in with your Oxy account will appear here", "Channels", "Connected apps", "Dark", "Email digest", "Failed to revoke access", "Failed to update notification preferences", "Following system: reduce motion is on", "Last used {{relative}}", "Light", "Manage push, email, and security alerts", "Manage third-party app access", "Marketing emails", "Minimise animations across Oxy apps", "No connected apps", "Notifications", "Periodic summary of your account activity", "Preferences", "Preferences sync across every Oxy app you sign into", "Product news and occasional offers", "Push notifications", "Real-time alerts on your devices", "Reduce motion", "Region", "Revoke", "Revoke access", "Revoke {{name}}'s access to your Oxy account?", "Revoked access for {{name}}", "Security alerts", "Sign-ins, recovery codes, and key changes", "System default", "Theme", "Theme, motion, and regional settings", "Timezone", "Unable to detect timezone", "yourname"];
|
|
2
|
+
export default _default;
|
|
@@ -29,6 +29,8 @@ export declare const HttpStatus: {
|
|
|
29
29
|
* @returns The status code if found, undefined otherwise
|
|
30
30
|
*/
|
|
31
31
|
export declare const getErrorStatus: (error: unknown) => number | undefined;
|
|
32
|
+
/** A locally superseded request is not an unauthorized response from the server. */
|
|
33
|
+
export declare const isSessionChangedError: (error: unknown) => boolean;
|
|
32
34
|
/**
|
|
33
35
|
* Extracts the error message from an error object.
|
|
34
36
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.2",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -163,16 +163,18 @@
|
|
|
163
163
|
"node": ">=20.0.0"
|
|
164
164
|
},
|
|
165
165
|
"scripts": {
|
|
166
|
-
"build": "node scripts/build-workspace-deps.mjs @oxy.so/telemetry && bun run build:cjs && bun run build:esm && bun run build:types && node scripts/mark-module-formats.mjs && node -e \"const p='../app-preset/metro/source-stamp.js';require('node:fs').existsSync(p)&&require(p).writeStamp(process.cwd())\"",
|
|
166
|
+
"build": "bun run check:compact-locales && node scripts/build-workspace-deps.mjs @oxy.so/telemetry && bun run build:cjs && bun run build:esm && bun run build:types && node scripts/mark-module-formats.mjs && node -e \"const p='../app-preset/metro/source-stamp.js';require('node:fs').existsSync(p)&&require(p).writeStamp(process.cwd())\"",
|
|
167
167
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
168
168
|
"build:esm": "tsc -p tsconfig.esm.json && node scripts/fix-esm-imports.mjs",
|
|
169
169
|
"build:types": "tsc -p tsconfig.types.json",
|
|
170
170
|
"clean": "rm -rf dist",
|
|
171
171
|
"typescript": "tsc --noEmit",
|
|
172
|
-
"test": "jest --passWithNoTests",
|
|
172
|
+
"test": "bun run check:compact-locales && jest --passWithNoTests",
|
|
173
173
|
"postbuild": "node scripts/verify-package-formats.mjs",
|
|
174
174
|
"verify:package": "node scripts/verify-package-formats.mjs",
|
|
175
175
|
"lint": "biome lint --error-on-warnings ./src",
|
|
176
|
+
"check:compact-locales": "bun scripts/generateCompactLocales.ts --check",
|
|
177
|
+
"generate:compact-locales": "bun scripts/generateCompactLocales.ts",
|
|
176
178
|
"generate:display-name-policy": "node scripts/generateDisplayNamePolicyRanges.mjs",
|
|
177
179
|
"test:bun": "cd bun-tests && bun test"
|
|
178
180
|
},
|
package/src/HttpService.ts
CHANGED
|
@@ -88,6 +88,17 @@ function sameTokenSession(left: string | null, right: string | null): boolean {
|
|
|
88
88
|
].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** Discard an unused body even when a platform's stream shim lacks cancel. */
|
|
92
|
+
function discardResponseBody(response: Response): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
const body = response.body;
|
|
95
|
+
if (typeof body?.cancel !== 'function') return Promise.resolve();
|
|
96
|
+
return Promise.resolve(body.cancel()).catch(() => {});
|
|
97
|
+
} catch {
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
/** A trusted platform transport, for example Expo's streaming fetch. */
|
|
92
103
|
export type ResponseTransport = (input: string, init: RequestInit) => Promise<Response>;
|
|
93
104
|
|
|
@@ -695,6 +706,13 @@ export class HttpService {
|
|
|
695
706
|
* {@link ResponsePayload}, which makes reading the envelope a compile error.
|
|
696
707
|
*/
|
|
697
708
|
async request<T = unknown>(config: RequestConfig): Promise<ResponsePayload<T>> {
|
|
709
|
+
if (config.signal?.aborted) throw createCancelledError('Request cancelled before it was sent');
|
|
710
|
+
// The provider can change independently of the last request. Synchronize
|
|
711
|
+
// before deriving the cache and deduplication identity, including hits.
|
|
712
|
+
const explicitAuthorization = Object.keys(config.headers ?? {}).some(
|
|
713
|
+
(key) => key.toLowerCase() === 'authorization',
|
|
714
|
+
);
|
|
715
|
+
if (!config.skipAuth && !explicitAuthorization) this.syncAccessTokenFromProvider();
|
|
698
716
|
// Per-call authority is not represented by the instance's session cache
|
|
699
717
|
// key. Never share or retain responses authenticated by explicit headers.
|
|
700
718
|
if (
|
|
@@ -725,7 +743,9 @@ export class HttpService {
|
|
|
725
743
|
valid: true,
|
|
726
744
|
});
|
|
727
745
|
}
|
|
728
|
-
const key =
|
|
746
|
+
const key =
|
|
747
|
+
this.generateCacheKey(config.method, config.url, config.data || config.params) +
|
|
748
|
+
(config.skipAuth ? ' auth=none' : '');
|
|
729
749
|
let lease = this.readLeases.get(key);
|
|
730
750
|
if (!lease) {
|
|
731
751
|
lease = { key, id: ++this.nextReadLeaseId, valid: true, readers: 0 };
|
|
@@ -768,6 +788,7 @@ export class HttpService {
|
|
|
768
788
|
// overrides any per-request `cache:true`: nothing is read from nor written
|
|
769
789
|
// to the response cache. Request deduplication below is unaffected.
|
|
770
790
|
const cache = cacheRequested && !this.cacheDisabled;
|
|
791
|
+
const cacheSessionEpoch = this.sessionEpoch;
|
|
771
792
|
|
|
772
793
|
// Generate cache key (optimized for large objects)
|
|
773
794
|
const cacheKey = cache ? lease.key : null;
|
|
@@ -795,7 +816,37 @@ export class HttpService {
|
|
|
795
816
|
// and skip the near-expiry preflight — see RequestOptions.skipAuth. The
|
|
796
817
|
// 401 retry re-enters request() with a fresh config, so it re-resolves
|
|
797
818
|
// this here with the refreshed token.
|
|
798
|
-
const
|
|
819
|
+
const explicitAuth = Object.keys(config.headers ?? {}).some(
|
|
820
|
+
(key) => key.toLowerCase() === 'authorization',
|
|
821
|
+
);
|
|
822
|
+
const usesSessionAuth = !config.skipAuth && !explicitAuth;
|
|
823
|
+
const contextToken = usesSessionAuth ? this.syncAccessTokenFromProvider() : null;
|
|
824
|
+
const contextEpoch = this.sessionEpoch;
|
|
825
|
+
const authHeader = usesSessionAuth
|
|
826
|
+
? await awaitRequestSignal(this.getAuthHeader(), signal)
|
|
827
|
+
: null;
|
|
828
|
+
const isOriginalSession = () =>
|
|
829
|
+
this.sessionEpoch === contextEpoch &&
|
|
830
|
+
sameTokenSession(contextToken, this.syncAccessTokenFromProvider());
|
|
831
|
+
const newerRequestToken = () => {
|
|
832
|
+
if (!contextToken || !authHeader?.startsWith('Bearer ')) return null;
|
|
833
|
+
const requestToken = authHeader.slice('Bearer '.length);
|
|
834
|
+
const current = this.syncAccessTokenFromProvider();
|
|
835
|
+
return current && current !== requestToken && sameTokenSession(requestToken, current)
|
|
836
|
+
? current
|
|
837
|
+
: null;
|
|
838
|
+
};
|
|
839
|
+
// The provider can change between cache-key selection and auth preflight.
|
|
840
|
+
// Refuse to attach a response to a lease belonging to another identity.
|
|
841
|
+
if (
|
|
842
|
+
usesSessionAuth &&
|
|
843
|
+
(!isOriginalSession() ||
|
|
844
|
+
(lease.key &&
|
|
845
|
+
(cacheSessionEpoch !== this.sessionEpoch ||
|
|
846
|
+
lease.key !== this.generateCacheKey(method, url, data || params))))
|
|
847
|
+
) {
|
|
848
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
849
|
+
}
|
|
799
850
|
// Never awaited: the PoP is a hint, and waiting on its trace fetch put up
|
|
800
851
|
// to a second in front of requests.
|
|
801
852
|
const edgeRegionHeader = peekBrowserEdgeRegionHeader();
|
|
@@ -820,6 +871,9 @@ export class HttpService {
|
|
|
820
871
|
if (domain.aborted) {
|
|
821
872
|
throw createCancelledError('Request cancelled');
|
|
822
873
|
}
|
|
874
|
+
if (usesSessionAuth && !isOriginalSession()) {
|
|
875
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
876
|
+
}
|
|
823
877
|
|
|
824
878
|
/**
|
|
825
879
|
* Whether OUR timer fired, so a timeout can be told apart from a
|
|
@@ -828,6 +882,7 @@ export class HttpService {
|
|
|
828
882
|
* an abandoned request as a transient failure.
|
|
829
883
|
*/
|
|
830
884
|
let timedOut = false;
|
|
885
|
+
let selfClearedEpoch: number | null = null;
|
|
831
886
|
|
|
832
887
|
// This attempt's controller carries its own timeout and inherits the
|
|
833
888
|
// call-wide abort domain. Declared OUTSIDE the `try` so the `finally`
|
|
@@ -955,14 +1010,29 @@ export class HttpService {
|
|
|
955
1010
|
// it, including on every throw between here and there.
|
|
956
1011
|
if (timeoutId) clearTimeout(timeoutId);
|
|
957
1012
|
|
|
1013
|
+
if (usesSessionAuth && !isOriginalSession()) {
|
|
1014
|
+
void discardResponseBody(response);
|
|
1015
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
958
1018
|
// Handle response
|
|
959
1019
|
if (!response.ok) {
|
|
960
1020
|
// On 401, delegate to the installed auth refresh handler and retry
|
|
961
1021
|
// once before giving up. HttpService deliberately does not know any
|
|
962
1022
|
// session routes; the refresh handler owns session rotation.
|
|
963
|
-
if (
|
|
964
|
-
|
|
965
|
-
|
|
1023
|
+
if (
|
|
1024
|
+
response.status === 401 &&
|
|
1025
|
+
!config._isAuthRetry &&
|
|
1026
|
+
usesSessionAuth &&
|
|
1027
|
+
isOriginalSession()
|
|
1028
|
+
) {
|
|
1029
|
+
let refreshed = newerRequestToken();
|
|
1030
|
+
if (!refreshed) {
|
|
1031
|
+
if (domain.aborted) throw createCancelledError();
|
|
1032
|
+
refreshed = await awaitRequestSignal(this.refreshAccessToken('response-401'), domain);
|
|
1033
|
+
}
|
|
1034
|
+
refreshed ||= newerRequestToken();
|
|
1035
|
+
if (refreshed && isOriginalSession()) {
|
|
966
1036
|
// `deduplicate: false` is REQUIRED on the retry. This re-issue runs
|
|
967
1037
|
// while the ORIGINAL request is still in-flight under its dedupe
|
|
968
1038
|
// key; the refreshed token is
|
|
@@ -981,11 +1051,18 @@ export class HttpService {
|
|
|
981
1051
|
retry: false,
|
|
982
1052
|
deduplicate: false,
|
|
983
1053
|
bypassQueue: true,
|
|
1054
|
+
// The original caller's signal may be aborted while another
|
|
1055
|
+
// deduplicated caller still owns this shared request.
|
|
1056
|
+
signal: domain,
|
|
984
1057
|
});
|
|
985
1058
|
}
|
|
986
1059
|
// Refresh failed or no token — clear tokens
|
|
987
|
-
|
|
988
|
-
|
|
1060
|
+
if (isOriginalSession()) {
|
|
1061
|
+
if (this.tokenStore.getAccessToken()) this.sessionEpoch += 1;
|
|
1062
|
+
this.tokenStore.clearTokens();
|
|
1063
|
+
selfClearedEpoch = this.sessionEpoch;
|
|
1064
|
+
this.notifyTokenChange();
|
|
1065
|
+
}
|
|
989
1066
|
}
|
|
990
1067
|
|
|
991
1068
|
// Read the error body (may be absent, non-JSON, empty or malformed).
|
|
@@ -1079,6 +1156,9 @@ export class HttpService {
|
|
|
1079
1156
|
responseData = text || null;
|
|
1080
1157
|
}
|
|
1081
1158
|
|
|
1159
|
+
if (usesSessionAuth && !isOriginalSession()) {
|
|
1160
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1161
|
+
}
|
|
1082
1162
|
const duration = Date.now() - startTime;
|
|
1083
1163
|
this.updateMetrics(true, duration);
|
|
1084
1164
|
this.config.onRequestEnd?.(url, method, duration, true);
|
|
@@ -1087,6 +1167,24 @@ export class HttpService {
|
|
|
1087
1167
|
} catch (error: unknown) {
|
|
1088
1168
|
const duration = Date.now() - startTime;
|
|
1089
1169
|
this.updateMetrics(false, duration);
|
|
1170
|
+
// A body reader can reject or finish parsing an error after the viewer
|
|
1171
|
+
// changed. Do not surface that previous viewer's error or callbacks.
|
|
1172
|
+
// An explicit abort/timeout retains its existing classification.
|
|
1173
|
+
const providerToken = selfClearedEpoch !== null ? this.accessTokenProvider?.() : null;
|
|
1174
|
+
const originalClearStillCurrent =
|
|
1175
|
+
selfClearedEpoch !== null &&
|
|
1176
|
+
selfClearedEpoch === this.sessionEpoch &&
|
|
1177
|
+
this.tokenStore.getAccessToken() === null &&
|
|
1178
|
+
(!providerToken || sameTokenSession(contextToken, providerToken));
|
|
1179
|
+
if (
|
|
1180
|
+
usesSessionAuth &&
|
|
1181
|
+
!originalClearStillCurrent &&
|
|
1182
|
+
!isOriginalSession() &&
|
|
1183
|
+
!isAbortLike(error) &&
|
|
1184
|
+
!controller.signal.aborted
|
|
1185
|
+
) {
|
|
1186
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1187
|
+
}
|
|
1090
1188
|
this.config.onRequestEnd?.(url, method, duration, false);
|
|
1091
1189
|
this.config.onRequestError?.(
|
|
1092
1190
|
url,
|
|
@@ -1188,6 +1286,10 @@ export class HttpService {
|
|
|
1188
1286
|
}
|
|
1189
1287
|
}
|
|
1190
1288
|
|
|
1289
|
+
if (usesSessionAuth && !isOriginalSession()) {
|
|
1290
|
+
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1191
1293
|
// Cache the result if caching is enabled
|
|
1192
1294
|
if (cache && cacheKey && lease.valid && result) {
|
|
1193
1295
|
this.cache.set(cacheKey, result, cacheTTL);
|
|
@@ -1382,7 +1484,7 @@ export class HttpService {
|
|
|
1382
1484
|
* in `clearCacheEntry`.
|
|
1383
1485
|
*/
|
|
1384
1486
|
private generateCacheKey(method: string, url: string, data?: unknown): string {
|
|
1385
|
-
return `${this.generateBaseCacheKey(method, url, data)}${HttpService.CACHE_IDENTITY_DELIM}${this.computeIdentityTag()}`;
|
|
1487
|
+
return `${this.generateBaseCacheKey(method, url, data)}${HttpService.CACHE_IDENTITY_DELIM}${this.computeIdentityTag()} epoch=${this.sessionEpoch}`;
|
|
1386
1488
|
}
|
|
1387
1489
|
|
|
1388
1490
|
/**
|
|
@@ -1516,6 +1618,7 @@ export class HttpService {
|
|
|
1516
1618
|
// on the way out would discard the flag it just set.
|
|
1517
1619
|
this.lastRefreshWasRateLimited = false;
|
|
1518
1620
|
const epoch = this.sessionEpoch;
|
|
1621
|
+
const tokenAtStart = this.tokenStore.getAccessToken();
|
|
1519
1622
|
this.tokenRefreshPromise = this.authRefreshHandler(reason)
|
|
1520
1623
|
.then((newToken) => {
|
|
1521
1624
|
if (epoch !== this.sessionEpoch) {
|
|
@@ -1528,6 +1631,12 @@ export class HttpService {
|
|
|
1528
1631
|
this.logger.debug('Discarded a token refresh that outlived its session');
|
|
1529
1632
|
return null;
|
|
1530
1633
|
}
|
|
1634
|
+
const installed = this.tokenStore.getAccessToken();
|
|
1635
|
+
if (installed !== tokenAtStart && installed !== newToken) {
|
|
1636
|
+
// Another operation installed a token while this refresh was in
|
|
1637
|
+
// flight. Never overwrite it with this older completion.
|
|
1638
|
+
return installed && sameTokenSession(tokenAtStart, installed) ? installed : null;
|
|
1639
|
+
}
|
|
1531
1640
|
if (!newToken) {
|
|
1532
1641
|
this.lastRefreshFailureAt = Date.now();
|
|
1533
1642
|
return null;
|
|
@@ -1865,7 +1974,7 @@ export class HttpService {
|
|
|
1865
1974
|
try {
|
|
1866
1975
|
assertCurrent();
|
|
1867
1976
|
} catch (error) {
|
|
1868
|
-
await response
|
|
1977
|
+
await discardResponseBody(response);
|
|
1869
1978
|
throw error;
|
|
1870
1979
|
}
|
|
1871
1980
|
const replayable =
|
|
@@ -1884,17 +1993,17 @@ export class HttpService {
|
|
|
1884
1993
|
config.signal,
|
|
1885
1994
|
);
|
|
1886
1995
|
} catch (error) {
|
|
1887
|
-
void response
|
|
1996
|
+
void discardResponseBody(response);
|
|
1888
1997
|
throw error;
|
|
1889
1998
|
}
|
|
1890
1999
|
try {
|
|
1891
2000
|
assertCurrent();
|
|
1892
2001
|
} catch (error) {
|
|
1893
|
-
await response
|
|
2002
|
+
await discardResponseBody(response);
|
|
1894
2003
|
throw error;
|
|
1895
2004
|
}
|
|
1896
2005
|
if (refreshed) {
|
|
1897
|
-
await response
|
|
2006
|
+
await discardResponseBody(response);
|
|
1898
2007
|
if (config.signal?.aborted) throw createCancelledError();
|
|
1899
2008
|
return this.requestResponseAttempt(
|
|
1900
2009
|
config,
|
|
@@ -2099,6 +2208,7 @@ export class HttpService {
|
|
|
2099
2208
|
*/
|
|
2100
2209
|
peekCachedGet<T>(url: string, params?: Record<string, unknown>): T | null {
|
|
2101
2210
|
if (this.cacheDisabled) return null;
|
|
2211
|
+
this.syncAccessTokenFromProvider();
|
|
2102
2212
|
return this.cache.get(this.generateCacheKey('GET', url, params)) as T | null;
|
|
2103
2213
|
}
|
|
2104
2214
|
|