@oxyhq/core 17.1.0 → 19.0.0
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/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +4 -5
- package/dist/cjs/i18n/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/es-ES.json +48 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +48 -5
- package/dist/cjs/index.js +10 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +27 -2
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/cjs/mixins/OxyServices.user.js +14 -20
- package/dist/cjs/server/index.js +8 -2
- package/dist/cjs/server/userInvalidation.js +6 -28
- package/dist/cjs/session/refresh.js +9 -14
- package/dist/cjs/utils/identityCacheSweep.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +4 -5
- package/dist/esm/i18n/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/es-ES.json +48 -5
- package/dist/esm/i18n/locales/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +48 -5
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +22 -1
- package/dist/esm/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/esm/mixins/OxyServices.user.js +14 -20
- package/dist/esm/server/index.js +5 -1
- package/dist/esm/server/userInvalidation.js +5 -26
- package/dist/esm/session/refresh.js +9 -14
- package/dist/esm/utils/identityCacheSweep.js +92 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +54 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +3 -2
- package/dist/types/mixins/OxyServices.user.d.ts +9 -7
- package/dist/types/models/interfaces.d.ts +11 -3
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/userInvalidation.d.ts +5 -24
- package/dist/types/session/refresh.d.ts +13 -18
- package/dist/types/utils/identityCacheSweep.d.ts +80 -0
- package/package.json +2 -2
- package/src/boot/sessionColdBoot.ts +4 -5
- package/src/i18n/locales/en-US.json +48 -5
- package/src/i18n/locales/es-ES.json +48 -5
- package/src/index.ts +8 -3
- package/src/mixins/OxyServices.accounts.ts +73 -12
- package/src/mixins/OxyServices.deviceBoot.ts +3 -2
- package/src/mixins/OxyServices.user.ts +14 -20
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +370 -0
- package/src/models/interfaces.ts +11 -3
- package/src/server/__tests__/userInvalidation.test.ts +3 -20
- package/src/server/index.ts +5 -2
- package/src/server/userInvalidation.ts +8 -36
- package/src/session/refresh.ts +15 -20
- package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
- package/src/utils/identityCacheSweep.ts +104 -0
|
@@ -134,10 +134,9 @@ async function runSessionColdBoot(opts) {
|
|
|
134
134
|
// origin persisted a deviceId + deviceSecret, mint a short access token with
|
|
135
135
|
// a single bearer-less POST (no cookie, no navigation). The mint itself runs
|
|
136
136
|
// through `refreshDeviceSecretArm`, which acquires the client's PROCESS-WIDE
|
|
137
|
-
// single-flight, persists
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// the durable store always converges on the true `current` secret.
|
|
137
|
+
// single-flight, persists `nextDeviceSecret` BEFORE planting the token, and
|
|
138
|
+
// returns a classified outcome — so concurrent mint lanes share one in-flight
|
|
139
|
+
// request and the durable store always converges on the server's credential.
|
|
141
140
|
steps.push({
|
|
142
141
|
id: 'device-secret-mint',
|
|
143
142
|
// Network step — skip entirely when the caller reports the device offline so
|
|
@@ -156,7 +155,7 @@ async function runSessionColdBoot(opts) {
|
|
|
156
155
|
const result = await (0, refresh_1.refreshDeviceSecretArm)({ oxy, store, pin });
|
|
157
156
|
switch (result.status) {
|
|
158
157
|
case 'ok':
|
|
159
|
-
// The arm persisted
|
|
158
|
+
// The arm persisted nextDeviceSecret and planted the token.
|
|
160
159
|
return {
|
|
161
160
|
kind: 'session',
|
|
162
161
|
session: {
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "News",
|
|
1753
|
+
"politics": "Politics & policy",
|
|
1754
|
+
"business": "Business",
|
|
1755
|
+
"startup": "Startups & entrepreneurship",
|
|
1756
|
+
"finance": "Finance & investing",
|
|
1757
|
+
"crypto": "Crypto & web3",
|
|
1758
|
+
"marketplace": "Marketplace & classifieds",
|
|
1759
|
+
"retail": "Retail & ecommerce",
|
|
1760
|
+
"real_estate": "Real estate",
|
|
1752
1761
|
"agency": "Real estate agency",
|
|
1762
|
+
"landlord": "Landlord & property management",
|
|
1753
1763
|
"cooperative": "Housing cooperative",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Architecture & construction",
|
|
1765
|
+
"technology": "Technology",
|
|
1766
|
+
"software": "Software & development",
|
|
1767
|
+
"ai": "AI & machine learning",
|
|
1768
|
+
"security": "Security & privacy",
|
|
1769
|
+
"automation": "Automation & tools",
|
|
1770
|
+
"science": "Science & research",
|
|
1771
|
+
"education": "Education & training",
|
|
1772
|
+
"books": "Books & writing",
|
|
1773
|
+
"health": "Health & medicine",
|
|
1774
|
+
"fitness": "Fitness & wellbeing",
|
|
1775
|
+
"sports": "Sports",
|
|
1776
|
+
"gaming": "Gaming & esports",
|
|
1777
|
+
"music": "Music",
|
|
1778
|
+
"film": "Film & TV",
|
|
1779
|
+
"podcast": "Podcasts & audio",
|
|
1780
|
+
"art": "Art & design",
|
|
1781
|
+
"photography": "Photography",
|
|
1782
|
+
"comedy": "Comedy",
|
|
1783
|
+
"food": "Food & drink",
|
|
1784
|
+
"travel": "Travel & places",
|
|
1785
|
+
"fashion": "Fashion & beauty",
|
|
1786
|
+
"home_garden": "Home & garden",
|
|
1787
|
+
"diy": "DIY & making",
|
|
1788
|
+
"automotive": "Cars & motoring",
|
|
1789
|
+
"animals": "Animals & pets",
|
|
1790
|
+
"family": "Family & parenting",
|
|
1791
|
+
"nonprofit": "Non-profit & charity",
|
|
1792
|
+
"government": "Government & public sector",
|
|
1793
|
+
"community": "Local community",
|
|
1794
|
+
"activism": "Activism & advocacy",
|
|
1795
|
+
"environment": "Environment & sustainability",
|
|
1796
|
+
"religion": "Religion & spirituality",
|
|
1797
|
+
"other": "Other"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Create account",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Only letters, numbers, hyphens, and underscores",
|
|
1773
1815
|
"checkFailed": "Could not check availability"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categories",
|
|
1819
|
+
"hint": "Pick up to {{max}}. The first one is your main category."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Account created",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "Una cuenta programática con credenciales de servicio"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "Noticias",
|
|
1753
|
+
"politics": "Política",
|
|
1754
|
+
"business": "Negocios",
|
|
1755
|
+
"startup": "Startups y emprendimiento",
|
|
1756
|
+
"finance": "Finanzas e inversión",
|
|
1757
|
+
"crypto": "Cripto y web3",
|
|
1758
|
+
"marketplace": "Marketplace y anuncios",
|
|
1759
|
+
"retail": "Comercio y ecommerce",
|
|
1760
|
+
"real_estate": "Inmobiliaria",
|
|
1752
1761
|
"agency": "Agencia inmobiliaria",
|
|
1762
|
+
"landlord": "Casero y gestión de fincas",
|
|
1753
1763
|
"cooperative": "Cooperativa de vivienda",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Arquitectura y construcción",
|
|
1765
|
+
"technology": "Tecnología",
|
|
1766
|
+
"software": "Software y desarrollo",
|
|
1767
|
+
"ai": "IA y aprendizaje automático",
|
|
1768
|
+
"security": "Seguridad y privacidad",
|
|
1769
|
+
"automation": "Automatización y herramientas",
|
|
1770
|
+
"science": "Ciencia e investigación",
|
|
1771
|
+
"education": "Educación y formación",
|
|
1772
|
+
"books": "Libros y escritura",
|
|
1773
|
+
"health": "Salud y medicina",
|
|
1774
|
+
"fitness": "Deporte y bienestar",
|
|
1775
|
+
"sports": "Deportes",
|
|
1776
|
+
"gaming": "Videojuegos y esports",
|
|
1777
|
+
"music": "Música",
|
|
1778
|
+
"film": "Cine y TV",
|
|
1779
|
+
"podcast": "Pódcast y audio",
|
|
1780
|
+
"art": "Arte y diseño",
|
|
1781
|
+
"photography": "Fotografía",
|
|
1782
|
+
"comedy": "Comedia",
|
|
1783
|
+
"food": "Comida y bebida",
|
|
1784
|
+
"travel": "Viajes y lugares",
|
|
1785
|
+
"fashion": "Moda y belleza",
|
|
1786
|
+
"home_garden": "Hogar y jardín",
|
|
1787
|
+
"diy": "Bricolaje y manualidades",
|
|
1788
|
+
"automotive": "Coches y motor",
|
|
1789
|
+
"animals": "Animales y mascotas",
|
|
1790
|
+
"family": "Familia y crianza",
|
|
1791
|
+
"nonprofit": "ONG y benéficas",
|
|
1792
|
+
"government": "Gobierno y sector público",
|
|
1793
|
+
"community": "Comunidad local",
|
|
1794
|
+
"activism": "Activismo y defensa",
|
|
1795
|
+
"environment": "Medio ambiente y sostenibilidad",
|
|
1796
|
+
"religion": "Religión y espiritualidad",
|
|
1797
|
+
"other": "Otra"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Crear cuenta",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Solo letras, números, guiones y guiones bajos",
|
|
1773
1815
|
"checkFailed": "No se pudo comprobar la disponibilidad"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categorías",
|
|
1819
|
+
"hint": "Elige hasta {{max}}. La primera es tu categoría principal."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Cuenta creada",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "News",
|
|
1753
|
+
"politics": "Politics & policy",
|
|
1754
|
+
"business": "Business",
|
|
1755
|
+
"startup": "Startups & entrepreneurship",
|
|
1756
|
+
"finance": "Finance & investing",
|
|
1757
|
+
"crypto": "Crypto & web3",
|
|
1758
|
+
"marketplace": "Marketplace & classifieds",
|
|
1759
|
+
"retail": "Retail & ecommerce",
|
|
1760
|
+
"real_estate": "Real estate",
|
|
1752
1761
|
"agency": "Real estate agency",
|
|
1762
|
+
"landlord": "Landlord & property management",
|
|
1753
1763
|
"cooperative": "Housing cooperative",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Architecture & construction",
|
|
1765
|
+
"technology": "Technology",
|
|
1766
|
+
"software": "Software & development",
|
|
1767
|
+
"ai": "AI & machine learning",
|
|
1768
|
+
"security": "Security & privacy",
|
|
1769
|
+
"automation": "Automation & tools",
|
|
1770
|
+
"science": "Science & research",
|
|
1771
|
+
"education": "Education & training",
|
|
1772
|
+
"books": "Books & writing",
|
|
1773
|
+
"health": "Health & medicine",
|
|
1774
|
+
"fitness": "Fitness & wellbeing",
|
|
1775
|
+
"sports": "Sports",
|
|
1776
|
+
"gaming": "Gaming & esports",
|
|
1777
|
+
"music": "Music",
|
|
1778
|
+
"film": "Film & TV",
|
|
1779
|
+
"podcast": "Podcasts & audio",
|
|
1780
|
+
"art": "Art & design",
|
|
1781
|
+
"photography": "Photography",
|
|
1782
|
+
"comedy": "Comedy",
|
|
1783
|
+
"food": "Food & drink",
|
|
1784
|
+
"travel": "Travel & places",
|
|
1785
|
+
"fashion": "Fashion & beauty",
|
|
1786
|
+
"home_garden": "Home & garden",
|
|
1787
|
+
"diy": "DIY & making",
|
|
1788
|
+
"automotive": "Cars & motoring",
|
|
1789
|
+
"animals": "Animals & pets",
|
|
1790
|
+
"family": "Family & parenting",
|
|
1791
|
+
"nonprofit": "Non-profit & charity",
|
|
1792
|
+
"government": "Government & public sector",
|
|
1793
|
+
"community": "Local community",
|
|
1794
|
+
"activism": "Activism & advocacy",
|
|
1795
|
+
"environment": "Environment & sustainability",
|
|
1796
|
+
"religion": "Religion & spirituality",
|
|
1797
|
+
"other": "Other"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Create account",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Only letters, numbers, hyphens, and underscores",
|
|
1773
1815
|
"checkFailed": "Could not check availability"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categories",
|
|
1819
|
+
"hint": "Pick up to {{max}}. The first one is your main category."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Account created",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "Una cuenta programática con credenciales de servicio"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "Noticias",
|
|
1753
|
+
"politics": "Política",
|
|
1754
|
+
"business": "Negocios",
|
|
1755
|
+
"startup": "Startups y emprendimiento",
|
|
1756
|
+
"finance": "Finanzas e inversión",
|
|
1757
|
+
"crypto": "Cripto y web3",
|
|
1758
|
+
"marketplace": "Marketplace y anuncios",
|
|
1759
|
+
"retail": "Comercio y ecommerce",
|
|
1760
|
+
"real_estate": "Inmobiliaria",
|
|
1752
1761
|
"agency": "Agencia inmobiliaria",
|
|
1762
|
+
"landlord": "Casero y gestión de fincas",
|
|
1753
1763
|
"cooperative": "Cooperativa de vivienda",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Arquitectura y construcción",
|
|
1765
|
+
"technology": "Tecnología",
|
|
1766
|
+
"software": "Software y desarrollo",
|
|
1767
|
+
"ai": "IA y aprendizaje automático",
|
|
1768
|
+
"security": "Seguridad y privacidad",
|
|
1769
|
+
"automation": "Automatización y herramientas",
|
|
1770
|
+
"science": "Ciencia e investigación",
|
|
1771
|
+
"education": "Educación y formación",
|
|
1772
|
+
"books": "Libros y escritura",
|
|
1773
|
+
"health": "Salud y medicina",
|
|
1774
|
+
"fitness": "Deporte y bienestar",
|
|
1775
|
+
"sports": "Deportes",
|
|
1776
|
+
"gaming": "Videojuegos y esports",
|
|
1777
|
+
"music": "Música",
|
|
1778
|
+
"film": "Cine y TV",
|
|
1779
|
+
"podcast": "Pódcast y audio",
|
|
1780
|
+
"art": "Arte y diseño",
|
|
1781
|
+
"photography": "Fotografía",
|
|
1782
|
+
"comedy": "Comedia",
|
|
1783
|
+
"food": "Comida y bebida",
|
|
1784
|
+
"travel": "Viajes y lugares",
|
|
1785
|
+
"fashion": "Moda y belleza",
|
|
1786
|
+
"home_garden": "Hogar y jardín",
|
|
1787
|
+
"diy": "Bricolaje y manualidades",
|
|
1788
|
+
"automotive": "Coches y motor",
|
|
1789
|
+
"animals": "Animales y mascotas",
|
|
1790
|
+
"family": "Familia y crianza",
|
|
1791
|
+
"nonprofit": "ONG y benéficas",
|
|
1792
|
+
"government": "Gobierno y sector público",
|
|
1793
|
+
"community": "Comunidad local",
|
|
1794
|
+
"activism": "Activismo y defensa",
|
|
1795
|
+
"environment": "Medio ambiente y sostenibilidad",
|
|
1796
|
+
"religion": "Religión y espiritualidad",
|
|
1797
|
+
"other": "Otra"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Crear cuenta",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Solo letras, números, guiones y guiones bajos",
|
|
1773
1815
|
"checkFailed": "No se pudo comprobar la disponibilidad"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categorías",
|
|
1819
|
+
"hint": "Elige hasta {{max}}. La primera es tu categoría principal."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Cuenta creada",
|
package/dist/cjs/index.js
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* If a symbol does not appear here, it is NOT part of the public API.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.
|
|
22
|
-
exports.
|
|
23
|
-
exports.
|
|
24
|
-
exports.
|
|
25
|
-
exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = void 0;
|
|
21
|
+
exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.RecoveryPhraseService = exports.SignatureService = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_CATEGORY_IDS = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.ServiceCredentialMismatchError = exports.oxyClient = exports.OXY_CLOUD_URL = exports.ServiceAssetMetadataError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.AssetUrlResolutionError = exports.OxyServices = void 0;
|
|
22
|
+
exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = void 0;
|
|
23
|
+
exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
|
|
24
|
+
exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = void 0;
|
|
25
|
+
exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = void 0;
|
|
26
26
|
// Ensure crypto polyfills are loaded before anything else
|
|
27
27
|
require("./crypto/polyfill");
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
@@ -66,7 +66,11 @@ Object.defineProperty(exports, "getNormalizedUserHandle", { enumerable: true, ge
|
|
|
66
66
|
var profileLinks_1 = require("./utils/profileLinks");
|
|
67
67
|
Object.defineProperty(exports, "normalizeProfileLinks", { enumerable: true, get: function () { return profileLinks_1.normalizeProfileLinks; } });
|
|
68
68
|
var OxyServices_accounts_1 = require("./mixins/OxyServices.accounts");
|
|
69
|
-
Object.defineProperty(exports, "
|
|
69
|
+
Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return OxyServices_accounts_1.ACCOUNT_CATEGORY_IDS; } });
|
|
70
|
+
Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return OxyServices_accounts_1.MAX_ACCOUNT_CATEGORIES; } });
|
|
71
|
+
Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return OxyServices_accounts_1.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
|
|
72
|
+
Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return OxyServices_accounts_1.isSelectableAccountCategoryId; } });
|
|
73
|
+
Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return OxyServices_accounts_1.kindAcceptsAccountCategories; } });
|
|
70
74
|
// ---------------------------------------------------------------------------
|
|
71
75
|
// Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
|
|
72
76
|
// The whole type family — the closed value sets, the two balance views and the
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isActAsEligibleKind = exports.
|
|
3
|
+
exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
|
|
4
4
|
exports.OxyServicesAccountsMixin = OxyServicesAccountsMixin;
|
|
5
5
|
const userIdentity_1 = require("../utils/userIdentity");
|
|
6
|
+
const identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
6
7
|
const mixinHelpers_1 = require("./mixinHelpers");
|
|
7
8
|
var contracts_1 = require("@oxyhq/contracts");
|
|
9
|
+
Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_1.ACCOUNT_CATEGORY_IDS; } });
|
|
8
10
|
Object.defineProperty(exports, "ACCOUNT_KINDS", { enumerable: true, get: function () { return contracts_1.ACCOUNT_KINDS; } });
|
|
9
|
-
Object.defineProperty(exports, "
|
|
11
|
+
Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return contracts_1.MAX_ACCOUNT_CATEGORIES; } });
|
|
12
|
+
Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_1.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
|
|
10
13
|
Object.defineProperty(exports, "isActAsEligibleKind", { enumerable: true, get: function () { return contracts_1.isActAsEligibleKind; } });
|
|
14
|
+
Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return contracts_1.isSelectableAccountCategoryId; } });
|
|
15
|
+
Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return contracts_1.kindAcceptsAccountCategories; } });
|
|
11
16
|
function OxyServicesAccountsMixin(Base) {
|
|
12
17
|
return class extends Base {
|
|
13
18
|
constructor(...args) {
|
|
@@ -165,6 +170,18 @@ function OxyServicesAccountsMixin(Base) {
|
|
|
165
170
|
/**
|
|
166
171
|
* Update an account's mutable profile fields. Tree placement changes
|
|
167
172
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
173
|
+
*
|
|
174
|
+
* An account IS a user, so this write changes identity — and a profile
|
|
175
|
+
* screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
|
|
176
|
+
* `GET /profiles/username/<handle>`, both cached for 5 minutes in the
|
|
177
|
+
* CALLER'S OWN process, so busting only the account-graph keys left every
|
|
178
|
+
* profile surface serving the pre-edit avatar and name for the full TTL
|
|
179
|
+
* with a perfectly healthy server (the cross-service `oxy:user:invalidate`
|
|
180
|
+
* signal does not help: it evicts BACKEND caches, and cannot reach a cache
|
|
181
|
+
* living in a browser tab). {@link evictOxyIdentityCache} owns that key
|
|
182
|
+
* list — see its docs for why the handle-keyed entries are prefix-swept
|
|
183
|
+
* (a RENAME leaves the old handle's entry unreachable by any targeted key).
|
|
184
|
+
*
|
|
168
185
|
* @param accountId - The account's Mongo `_id`.
|
|
169
186
|
* @param data - Subset of updatable profile fields.
|
|
170
187
|
*/
|
|
@@ -175,6 +192,14 @@ function OxyServicesAccountsMixin(Base) {
|
|
|
175
192
|
// data) so neither serves the pre-update snapshot.
|
|
176
193
|
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
177
194
|
this._invalidateAccountLists();
|
|
195
|
+
// The parent's children list embeds this account's profile and is keyed
|
|
196
|
+
// by the PARENT id, so it is reachable only from the response node.
|
|
197
|
+
const parentAccountId = res.account?.parentAccountId;
|
|
198
|
+
if (parentAccountId) {
|
|
199
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(parentAccountId)}/children`);
|
|
200
|
+
}
|
|
201
|
+
// Every identity read of this account, whichever key it lands under.
|
|
202
|
+
(0, identityCacheSweep_1.evictOxyIdentityCache)(this, accountId);
|
|
178
203
|
return res.account;
|
|
179
204
|
}
|
|
180
205
|
catch (error) {
|
|
@@ -65,8 +65,9 @@ function OxyServicesDeviceBootMixin(Base) {
|
|
|
65
65
|
* Zero-cookie mint. Present the first-party `deviceId` + `deviceSecret` to
|
|
66
66
|
* `POST /session/device/token` — NO bearer, NO cookies: possession of the
|
|
67
67
|
* secret IS the device-ownership proof. Returns a fresh short access token
|
|
68
|
-
* for the device's active account plus `nextDeviceSecret` (
|
|
69
|
-
*
|
|
68
|
+
* for the device's active account plus `nextDeviceSecret` (on mint, the same
|
|
69
|
+
* proven secret echoed back — rotation happens on sign-in, not mint) and
|
|
70
|
+
* the projected device-session `state`.
|
|
70
71
|
*
|
|
71
72
|
* `skipAuth`: this call carries no bearer, so a 401 must surface DIRECTLY —
|
|
72
73
|
* never trigger `HttpService`'s 401→refresh→retry dance. The cold boot / re-
|
|
@@ -6,6 +6,7 @@ const apiUtils_1 = require("../utils/apiUtils");
|
|
|
6
6
|
const keyManager_1 = require("../crypto/keyManager");
|
|
7
7
|
const signatureService_1 = require("../crypto/signatureService");
|
|
8
8
|
const userIdentity_1 = require("../utils/userIdentity");
|
|
9
|
+
const identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
9
10
|
const logger_1 = require("../logger");
|
|
10
11
|
const errorUtils_1 = require("../utils/errorUtils");
|
|
11
12
|
/**
|
|
@@ -347,28 +348,22 @@ function OxyServicesUserMixin(Base) {
|
|
|
347
348
|
/**
|
|
348
349
|
* Update user profile.
|
|
349
350
|
*
|
|
350
|
-
* Invalidates the SDK-side response cache for every endpoint that
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
351
|
+
* Invalidates the SDK-side response cache for every endpoint that can
|
|
352
|
+
* return this user — the list is owned by {@link evictOxyIdentityCache}, so
|
|
353
|
+
* a new identity read is added in one place instead of to each writer
|
|
354
|
+
* separately (this method's own hand-written copy had already drifted from
|
|
355
|
+
* the server-side one, missing `GET /auth/lookup/*` and
|
|
356
|
+
* `GET /profiles/resolve`). Without the sweep a follow-up
|
|
357
|
+
* `getUserBySession` inside the cache window returns the pre-update user —
|
|
358
|
+
* most visibly during onboarding, where the username step flickers back as
|
|
359
|
+
* if nothing was saved.
|
|
357
360
|
*
|
|
358
361
|
* TanStack Query handles offline queuing automatically.
|
|
359
362
|
*/
|
|
360
363
|
async updateProfile(updates) {
|
|
361
364
|
try {
|
|
362
365
|
const result = (0, userIdentity_1.normalizeUserIdentity)(await this.makeRequest('PUT', '/users/me', updates, { cache: false }));
|
|
363
|
-
|
|
364
|
-
// prefix sweep rather than an enumeration because the SDK never
|
|
365
|
-
// tracks the set of active session IDs centrally.
|
|
366
|
-
this.clearCacheByPrefix('GET:/session/user/');
|
|
367
|
-
this.clearCacheByPrefix('GET:/users/me');
|
|
368
|
-
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
369
|
-
if (result?.id) {
|
|
370
|
-
this.clearCacheEntry(`GET:/users/${result.id}`);
|
|
371
|
-
}
|
|
366
|
+
(0, identityCacheSweep_1.evictOxyIdentityCache)(this, result?.id);
|
|
372
367
|
return result;
|
|
373
368
|
}
|
|
374
369
|
catch (error) {
|
|
@@ -421,10 +416,9 @@ function OxyServicesUserMixin(Base) {
|
|
|
421
416
|
const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
422
417
|
cache: false,
|
|
423
418
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.clearCacheEntry(`GET:/users/${id}`);
|
|
419
|
+
// Privacy settings ride the user DTO, so every identity read goes stale
|
|
420
|
+
// too — same key list as any other profile write.
|
|
421
|
+
(0, identityCacheSweep_1.evictOxyIdentityCache)(this, id);
|
|
428
422
|
this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
|
|
429
423
|
return result;
|
|
430
424
|
}
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.isOfficialWebOrigin = exports.registrableApex = exports.
|
|
19
|
+
exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = exports.OXY_CSP_BASELINE = exports.formatOxyCspPolicy = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
|
|
20
20
|
var auth_1 = require("./auth");
|
|
21
21
|
Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
|
|
22
22
|
Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
|
|
@@ -59,8 +59,14 @@ Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function
|
|
|
59
59
|
// changes, every consuming backend sweeps its caches instead of waiting out a TTL.
|
|
60
60
|
var userInvalidation_1 = require("./userInvalidation");
|
|
61
61
|
Object.defineProperty(exports, "createOxyUserInvalidationHandler", { enumerable: true, get: function () { return userInvalidation_1.createOxyUserInvalidationHandler; } });
|
|
62
|
-
Object.defineProperty(exports, "evictOxyIdentityCache", { enumerable: true, get: function () { return userInvalidation_1.evictOxyIdentityCache; } });
|
|
63
62
|
Object.defineProperty(exports, "publishOxyUserInvalidation", { enumerable: true, get: function () { return userInvalidation_1.publishOxyUserInvalidation; } });
|
|
63
|
+
// The identity-key enumeration itself is platform-neutral (`src/utils/`) so the
|
|
64
|
+
// client mixins and this Node-only subscriber sweep the SAME list — a second
|
|
65
|
+
// copy is what let `updateAccount` and `updateProfile` drift apart.
|
|
66
|
+
var identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
67
|
+
Object.defineProperty(exports, "evictOxyIdentityCache", { enumerable: true, get: function () { return identityCacheSweep_1.evictOxyIdentityCache; } });
|
|
68
|
+
Object.defineProperty(exports, "oxyUserByIdCacheKey", { enumerable: true, get: function () { return identityCacheSweep_1.oxyUserByIdCacheKey; } });
|
|
69
|
+
Object.defineProperty(exports, "OXY_IDENTITY_CACHE_PREFIXES", { enumerable: true, get: function () { return identityCacheSweep_1.OXY_IDENTITY_CACHE_PREFIXES; } });
|
|
64
70
|
// Registrable-apex (eTLD+1) derivation via the Public Suffix List — the SINGLE
|
|
65
71
|
// SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
|
|
66
72
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* Every Oxy backend caches Oxy identity, and none of them find out when it
|
|
8
8
|
* changes. The `OxyServices` GET response cache holds `GET /users/:id` and
|
|
9
9
|
* `GET /profiles/username/:name` for five minutes; it is swept when THIS process
|
|
10
|
-
* writes the profile (
|
|
11
|
-
* never when somebody else does — which is the normal case, since
|
|
12
|
-
* edited in Oxy's own apps. So an avatar or display-name change is
|
|
13
|
-
* every consuming backend for up to five minutes, per process.
|
|
10
|
+
* writes the profile (the `evictOxyIdentityCache` calls in the user and accounts
|
|
11
|
+
* mixins) and never when somebody else does — which is the normal case, since
|
|
12
|
+
* profiles are edited in Oxy's own apps. So an avatar or display-name change is
|
|
13
|
+
* invisible to every consuming backend for up to five minutes, per process.
|
|
14
14
|
*
|
|
15
15
|
* oxy-api broadcasts {@link OXY_USER_INVALIDATION_CHANNEL} on the shared Valkey
|
|
16
16
|
* when a user's identity changes. This module is the consumer half: it parses
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
55
|
exports.publishOxyUserInvalidation = publishOxyUserInvalidation;
|
|
56
56
|
exports.createOxyUserInvalidationHandler = createOxyUserInvalidationHandler;
|
|
57
|
-
exports.evictOxyIdentityCache = evictOxyIdentityCache;
|
|
58
57
|
const contracts_1 = require("@oxyhq/contracts");
|
|
58
|
+
const identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
59
59
|
/**
|
|
60
60
|
* Broadcast that an Oxy user's record changed.
|
|
61
61
|
*
|
|
@@ -134,7 +134,7 @@ function createOxyUserInvalidationHandler(options = {}) {
|
|
|
134
134
|
}
|
|
135
135
|
if (oxy) {
|
|
136
136
|
try {
|
|
137
|
-
evictOxyIdentityCache(oxy, event.userId);
|
|
137
|
+
(0, identityCacheSweep_1.evictOxyIdentityCache)(oxy, event.userId);
|
|
138
138
|
}
|
|
139
139
|
catch (error) {
|
|
140
140
|
// A cache sweep must never cost us the app-specific eviction below.
|
|
@@ -154,25 +154,3 @@ function createOxyUserInvalidationHandler(options = {}) {
|
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
159
|
-
* given user's identity.
|
|
160
|
-
*
|
|
161
|
-
* The by-id entry is exact. The by-username and resolve entries are keyed by
|
|
162
|
-
* HANDLE, which cannot be derived from an id without the very lookup we are
|
|
163
|
-
* invalidating, so those are swept by prefix — the same imprecision the SDK
|
|
164
|
-
* already accepts when it sweeps its own cache after a local profile write, and
|
|
165
|
-
* bounded by the fact that over-eviction costs a refetch and can never serve
|
|
166
|
-
* wrong data.
|
|
167
|
-
*/
|
|
168
|
-
function evictOxyIdentityCache(oxy, userId) {
|
|
169
|
-
// Match the sweep the user mixin runs after a local profile write — session-
|
|
170
|
-
// bound and /users/me entries are keyed without the user id, so they must be
|
|
171
|
-
// prefix-swept on cross-service invalidation too.
|
|
172
|
-
oxy.clearCacheByPrefix('GET:/session/user/');
|
|
173
|
-
oxy.clearCacheByPrefix('GET:/users/me');
|
|
174
|
-
oxy.clearCacheByPrefix('GET:/auth/lookup/');
|
|
175
|
-
oxy.clearCacheEntry(`GET:/users/${userId}`);
|
|
176
|
-
oxy.clearCacheByPrefix('GET:/profiles/username/');
|
|
177
|
-
oxy.clearCacheByPrefix('GET:/profiles/resolve');
|
|
178
|
-
}
|
|
@@ -40,19 +40,14 @@ const MIN_SCHEDULE_DELAY_MS = 1000;
|
|
|
40
40
|
const MIN_FAILURE_BACKOFF_MS = 5000;
|
|
41
41
|
const MAX_FAILURE_BACKOFF_MS = 5 * 60000;
|
|
42
42
|
/**
|
|
43
|
-
* Arm 1 — the
|
|
44
|
-
*
|
|
43
|
+
* Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
|
|
44
|
+
* single-flight (`httpService.runSingleFlightDeviceSecretMint`).
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* SUPERSEDED secret — after the grace window the next cold boot mint 401s and the
|
|
52
|
-
* user is signed out. Routing EVERY lane through this one single-flight makes
|
|
53
|
-
* concurrent callers await the SAME in-flight mint and all receive its result, so
|
|
54
|
-
* there is exactly one rotation and the store always converges on the true
|
|
55
|
-
* `current` secret.
|
|
46
|
+
* Concurrent lanes (cold boot, the proactive scheduler, a request-time preflight,
|
|
47
|
+
* a 401 retry, the socket token transport, or a tab-focus reconcile) must not
|
|
48
|
+
* each persist a different view of the mint response. Routing EVERY lane through
|
|
49
|
+
* this one single-flight makes concurrent callers await the SAME in-flight mint
|
|
50
|
+
* and all receive its result, so the durable store converges on one credential.
|
|
56
51
|
*
|
|
57
52
|
* On success it persists `nextDeviceSecret` (read-back-verified) BEFORE planting
|
|
58
53
|
* the access token; a failed durable persist yields `persist-failed` WITHOUT
|
|
@@ -123,8 +118,8 @@ async function refreshDeviceSecretArm(deps) {
|
|
|
123
118
|
expiresAt: mint.expiresAt,
|
|
124
119
|
...(bound ? { sessionId: bound.sessionId, userId: bound.accountId } : {}),
|
|
125
120
|
};
|
|
126
|
-
//
|
|
127
|
-
//
|
|
121
|
+
// Persist nextDeviceSecret (read-back-verified) BEFORE planting the token.
|
|
122
|
+
// A failed durable persist must NOT plant.
|
|
128
123
|
const persistedOk = await store.save(next);
|
|
129
124
|
if (!persistedOk) {
|
|
130
125
|
return { status: 'persist-failed' };
|