@oxyhq/core 5.1.0 → 5.2.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.
Files changed (60) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +27 -5
  3. package/dist/cjs/OxyServices.base.js +71 -0
  4. package/dist/cjs/index.js +3 -1
  5. package/dist/cjs/mixins/OxyServices.accounts.js +10 -25
  6. package/dist/cjs/mixins/OxyServices.assets.js +76 -0
  7. package/dist/cjs/mixins/OxyServices.user.js +30 -11
  8. package/dist/cjs/utils/ssoBounce.js +59 -0
  9. package/dist/esm/.tsbuildinfo +1 -1
  10. package/dist/esm/AuthManager.js +27 -5
  11. package/dist/esm/OxyServices.base.js +71 -0
  12. package/dist/esm/index.js +1 -1
  13. package/dist/esm/mixins/OxyServices.accounts.js +10 -25
  14. package/dist/esm/mixins/OxyServices.assets.js +76 -0
  15. package/dist/esm/mixins/OxyServices.user.js +30 -11
  16. package/dist/esm/utils/ssoBounce.js +57 -0
  17. package/dist/types/.tsbuildinfo +1 -1
  18. package/dist/types/OxyServices.base.d.ts +38 -0
  19. package/dist/types/index.d.ts +2 -2
  20. package/dist/types/mixins/OxyServices.accounts.d.ts +1 -0
  21. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  22. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  23. package/dist/types/mixins/OxyServices.assets.d.ts +36 -1
  24. package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
  25. package/dist/types/mixins/OxyServices.civic.d.ts +1 -0
  26. package/dist/types/mixins/OxyServices.connectedApps.d.ts +1 -0
  27. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  28. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  29. package/dist/types/mixins/OxyServices.features.d.ts +1 -0
  30. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  31. package/dist/types/mixins/OxyServices.identity.d.ts +1 -0
  32. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  33. package/dist/types/mixins/OxyServices.links.d.ts +1 -0
  34. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  35. package/dist/types/mixins/OxyServices.nodes.d.ts +1 -0
  36. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  37. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  38. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  39. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -0
  40. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  41. package/dist/types/mixins/OxyServices.silent.d.ts +1 -0
  42. package/dist/types/mixins/OxyServices.sso.d.ts +1 -0
  43. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  44. package/dist/types/mixins/OxyServices.user.d.ts +34 -10
  45. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  46. package/dist/types/models/interfaces.d.ts +23 -0
  47. package/dist/types/utils/ssoBounce.d.ts +47 -0
  48. package/package.json +1 -1
  49. package/src/AuthManager.ts +29 -5
  50. package/src/OxyServices.base.ts +79 -0
  51. package/src/__tests__/establishDeviceRefreshSlot.test.ts +221 -0
  52. package/src/index.ts +3 -0
  53. package/src/mixins/OxyServices.accounts.ts +10 -33
  54. package/src/mixins/OxyServices.assets.ts +92 -1
  55. package/src/mixins/OxyServices.user.ts +43 -11
  56. package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +135 -0
  57. package/src/mixins/__tests__/getUsersByIds.test.ts +149 -0
  58. package/src/models/interfaces.ts +24 -0
  59. package/src/utils/__tests__/ssoBounce.test.ts +41 -0
  60. package/src/utils/ssoBounce.ts +61 -0
@@ -142,6 +142,44 @@ export declare class OxyServicesBase {
142
142
  * Get the raw access token (for constructing anchor URLs when needed)
143
143
  */
144
144
  getAccessToken(): string | null;
145
+ /**
146
+ * Register the CURRENTLY-ACTIVE session in the device's first-party
147
+ * multi-account refresh-cookie set by calling `POST /auth/session`.
148
+ *
149
+ * This is the single, shared primitive every web primary-session commit and the
150
+ * account switch use to plant their `oxy_rt_<authuser>` slot. It MUST be a
151
+ * dedicated call to `/auth/session` rather than relying on whichever endpoint
152
+ * established the session: that endpoint is frequently OUTSIDE the cookie's
153
+ * `Path=/auth` scope (`/accounts/:id/switch`) or is a cross-origin/credential-
154
+ * less restore (`/sso/exchange`, the IdP `/auth/silent` postMessage) that cannot
155
+ * set an `api.oxy.so` cookie at all. `/auth/session` runs where the device's
156
+ * existing slots ARE visible, so the server resolves this user's slot (reusing an
157
+ * existing one or allocating a new one) without clobbering a sibling account,
158
+ * mints a fresh access token bound to the same session, and returns the resolved
159
+ * `authuser`.
160
+ *
161
+ * Behaviour:
162
+ * - Requires a planted bearer (the caller must have already installed the
163
+ * session's access token); `/auth/session` derives the session from it.
164
+ * - On success re-plants the rotated access token (so the active token matches
165
+ * the freshly-rotated cookie) and returns the device `authuser` slot.
166
+ * - WEB-ONLY: on native there are no first-party refresh cookies → returns
167
+ * `null`.
168
+ * - FIRST-PARTY-ONLY: the cookie is host-only on the API host with
169
+ * `SameSite=Lax`, so it only sticks when the page is SAME-SITE (same
170
+ * registrable apex) as the API. On a cross-apex RP (`mention.earth` calling
171
+ * `api.oxy.so`) the browser rejects the `Set-Cookie` as a third-party cookie,
172
+ * so a returned slot would be a phantom never enumerated by `refresh-all`.
173
+ * Those RPs durably restore via the per-apex `/auth/silent` iframe + `/sso`
174
+ * bounce, NOT this device set → returns `null` without calling the API.
175
+ * - BEST-EFFORT: a failure (e.g. transient network) never throws — the session
176
+ * stays active in-memory; only its reload durability via the device set is at
177
+ * risk. The caller treats `null` as "not registered in the device set".
178
+ *
179
+ * @returns The resolved device `authuser` slot, or `null` on native / cross-apex
180
+ * / failure.
181
+ */
182
+ establishDeviceRefreshSlot(): Promise<number | null>;
145
183
  /**
146
184
  * Decode the current access token and return its `exp` claim in SECONDS since
147
185
  * the Unix epoch (the raw JWT `exp` unit), or `null` when there is no token,
@@ -62,7 +62,7 @@ export { RecoveryPhraseService } from './crypto/recoveryPhrase';
62
62
  export type { RecoveryPhraseResult } from './crypto/recoveryPhrase';
63
63
  export { DeviceManager } from './utils/deviceManager';
64
64
  export type { DeviceFingerprint, StoredDeviceInfo } from './utils/deviceManager';
65
- export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, ServiceAssetMetadata, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceSession, DeviceSessionsResponse, DeviceSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
65
+ export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, ServiceAssetMetadata, ServiceAssetMetadataBySha, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceSession, DeviceSessionsResponse, DeviceSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
66
66
  export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
67
67
  export { TopicType, TopicSource } from './models/Topic';
68
68
  export type { TopicData, TopicTranslation } from './models/Topic';
@@ -93,7 +93,7 @@ export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './uti
93
93
  export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn';
94
94
  export type { SsoReturnKind, SsoReturnResult, ConsumeSsoReturnDeps } from './utils/ssoReturn';
95
95
  export { generateSsoState } from './mixins/OxyServices.sso';
96
- export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoPriorSessionKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, allowSsoBounce, } from './utils/ssoBounce';
96
+ export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoPriorSessionKey, ssoSignedOutKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, silentRestoreSuppressed, allowSsoBounce, } from './utils/ssoBounce';
97
97
  export type { SsoBounceGate } from './utils/ssoBounce';
98
98
  export { runColdBoot } from './utils/coldBoot';
99
99
  export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
@@ -684,6 +684,7 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
684
684
  getCurrentUserId(): string | null;
685
685
  hasValidToken(): boolean;
686
686
  getAccessToken(): string | null;
687
+ establishDeviceRefreshSlot(): Promise<number | null>;
687
688
  getAccessTokenExpiry(): number | null;
688
689
  waitForAuth(timeoutMs?: number): Promise<boolean>;
689
690
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -54,6 +54,7 @@ export declare function OxyServicesAnalyticsMixin<T extends typeof OxyServicesBa
54
54
  getCurrentUserId(): string | null;
55
55
  hasValidToken(): boolean;
56
56
  getAccessToken(): string | null;
57
+ establishDeviceRefreshSlot(): Promise<number | null>;
57
58
  getAccessTokenExpiry(): number | null;
58
59
  waitForAuth(timeoutMs?: number): Promise<boolean>;
59
60
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -90,6 +90,7 @@ export declare function OxyServicesAppDataMixin<T extends typeof OxyServicesBase
90
90
  getCurrentUserId(): string | null;
91
91
  hasValidToken(): boolean;
92
92
  getAccessToken(): string | null;
93
+ establishDeviceRefreshSlot(): Promise<number | null>;
93
94
  getAccessTokenExpiry(): number | null;
94
95
  waitForAuth(timeoutMs?: number): Promise<boolean>;
95
96
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -1,4 +1,4 @@
1
- import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant, ServiceAssetMetadata } from '../models/interfaces';
1
+ import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant, ServiceAssetMetadata, ServiceAssetMetadataBySha } from '../models/interfaces';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
3
  export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T): {
4
4
  new (...args: any[]): {
@@ -90,6 +90,40 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
90
90
  * rate), mirroring the sibling service/POST methods which never cache.
91
91
  */
92
92
  getServiceAssetMetadataByIds(ids: string[]): Promise<ServiceAssetMetadata[]>;
93
+ /**
94
+ * Reverse content-address lookup: resolve many content `sha256` digests to
95
+ * the servable Oxy asset holding each, in one round-trip per chunk via
96
+ * `POST /assets/service/by-sha256` (body `{ sha256s }`).
97
+ *
98
+ * This is the INVERSE of {@link getServiceAssetMetadataByIds}: given a
99
+ * record's `blob.sha256`, it returns the asset's `id`, `mime`, byte `size`,
100
+ * `status`, and — for active, public, CDN-reachable assets only — a public
101
+ * `url` (`cloud.oxy.so`). Built for server-to-server callers (e.g. Mention's
102
+ * MTN materializer / node-blob sync) that hold a content hash and need to
103
+ * map it back to a servable asset. Hashes are lowercased, validated against
104
+ * a 64-char hex pattern (malformed entries dropped client-side), and
105
+ * deduplicated before being split into chunks of
106
+ * {@link SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE} (the server-side cap). The
107
+ * server omits unknown/deleted hashes from each chunk's `data`, so the
108
+ * merged result may be shorter than the requested list and the caller is
109
+ * expected to map by `sha256`.
110
+ *
111
+ * **Service-token auth (required).** `/assets/service/by-sha256` is guarded
112
+ * by `serviceAuthMiddleware` + the `files:read` scope and is called via
113
+ * `makeServiceRequest` (`Authorization: Bearer <serviceToken>`, `cache:false`).
114
+ * The calling client MUST be service-configured (`configureServiceAuth`)
115
+ * before invoking; a plain user-session request is rejected by the route's
116
+ * service-auth guard.
117
+ *
118
+ * Resilience: chunks are independent. A failed chunk is logged and skipped —
119
+ * every entry that resolved is still returned. An empty input (or one whose
120
+ * every value is malformed) resolves immediately with `[]` and performs no
121
+ * network call.
122
+ *
123
+ * Not cached at the SDK layer: it's a POST keyed on a multi-hash body (low
124
+ * hit rate), mirroring the sibling service/POST methods which never cache.
125
+ */
126
+ getServiceAssetMetadataBySha256(sha256s: string[]): Promise<ServiceAssetMetadataBySha[]>;
93
127
  /**
94
128
  * Upload raw file data
95
129
  */
@@ -175,6 +209,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
175
209
  getCurrentUserId(): string | null;
176
210
  hasValidToken(): boolean;
177
211
  getAccessToken(): string | null;
212
+ establishDeviceRefreshSlot(): Promise<number | null>;
178
213
  getAccessTokenExpiry(): number | null;
179
214
  waitForAuth(timeoutMs?: number): Promise<boolean>;
180
215
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -569,6 +569,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
569
569
  getCurrentUserId(): string | null;
570
570
  hasValidToken(): boolean;
571
571
  getAccessToken(): string | null;
572
+ establishDeviceRefreshSlot(): Promise<number | null>;
572
573
  getAccessTokenExpiry(): number | null;
573
574
  waitForAuth(timeoutMs?: number): Promise<boolean>;
574
575
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -492,6 +492,7 @@ export declare function OxyServicesCivicMixin<T extends typeof OxyServicesBase>(
492
492
  getCurrentUserId(): string | null;
493
493
  hasValidToken(): boolean;
494
494
  getAccessToken(): string | null;
495
+ establishDeviceRefreshSlot(): Promise<number | null>;
495
496
  getAccessTokenExpiry(): number | null;
496
497
  waitForAuth(timeoutMs?: number): Promise<boolean>;
497
498
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -147,6 +147,7 @@ export declare function OxyServicesConnectedAppsMixin<T extends typeof OxyServic
147
147
  getCurrentUserId(): string | null;
148
148
  hasValidToken(): boolean;
149
149
  getAccessToken(): string | null;
150
+ establishDeviceRefreshSlot(): Promise<number | null>;
150
151
  getAccessTokenExpiry(): number | null;
151
152
  waitForAuth(timeoutMs?: number): Promise<boolean>;
152
153
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -82,6 +82,7 @@ export declare function OxyServicesContactsMixin<T extends typeof OxyServicesBas
82
82
  getCurrentUserId(): string | null;
83
83
  hasValidToken(): boolean;
84
84
  getAccessToken(): string | null;
85
+ establishDeviceRefreshSlot(): Promise<number | null>;
85
86
  getAccessTokenExpiry(): number | null;
86
87
  waitForAuth(timeoutMs?: number): Promise<boolean>;
87
88
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -84,6 +84,7 @@ export declare function OxyServicesDevicesMixin<T extends typeof OxyServicesBase
84
84
  getCurrentUserId(): string | null;
85
85
  hasValidToken(): boolean;
86
86
  getAccessToken(): string | null;
87
+ establishDeviceRefreshSlot(): Promise<number | null>;
87
88
  getAccessTokenExpiry(): number | null;
88
89
  waitForAuth(timeoutMs?: number): Promise<boolean>;
89
90
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -232,6 +232,7 @@ export declare function OxyServicesFeaturesMixin<T extends typeof OxyServicesBas
232
232
  getCurrentUserId(): string | null;
233
233
  hasValidToken(): boolean;
234
234
  getAccessToken(): string | null;
235
+ establishDeviceRefreshSlot(): Promise<number | null>;
235
236
  getAccessTokenExpiry(): number | null;
236
237
  waitForAuth(timeoutMs?: number): Promise<boolean>;
237
238
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -286,6 +286,7 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
286
286
  getCurrentUserId(): string | null;
287
287
  hasValidToken(): boolean;
288
288
  getAccessToken(): string | null;
289
+ establishDeviceRefreshSlot(): Promise<number | null>;
289
290
  getAccessTokenExpiry(): number | null;
290
291
  waitForAuth(timeoutMs?: number): Promise<boolean>;
291
292
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -234,6 +234,7 @@ export declare function OxyServicesIdentityMixin<T extends typeof OxyServicesBas
234
234
  getCurrentUserId(): string | null;
235
235
  hasValidToken(): boolean;
236
236
  getAccessToken(): string | null;
237
+ establishDeviceRefreshSlot(): Promise<number | null>;
237
238
  getAccessTokenExpiry(): number | null;
238
239
  waitForAuth(timeoutMs?: number): Promise<boolean>;
239
240
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -69,6 +69,7 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
69
69
  getCurrentUserId(): string | null;
70
70
  hasValidToken(): boolean;
71
71
  getAccessToken(): string | null;
72
+ establishDeviceRefreshSlot(): Promise<number | null>;
72
73
  getAccessTokenExpiry(): number | null;
73
74
  waitForAuth(timeoutMs?: number): Promise<boolean>;
74
75
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -86,6 +86,7 @@ export declare function OxyServicesLinksMixin<T extends typeof OxyServicesBase>(
86
86
  getCurrentUserId(): string | null;
87
87
  hasValidToken(): boolean;
88
88
  getAccessToken(): string | null;
89
+ establishDeviceRefreshSlot(): Promise<number | null>;
89
90
  getAccessTokenExpiry(): number | null;
90
91
  waitForAuth(timeoutMs?: number): Promise<boolean>;
91
92
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -52,6 +52,7 @@ export declare function OxyServicesLocationMixin<T extends typeof OxyServicesBas
52
52
  getCurrentUserId(): string | null;
53
53
  hasValidToken(): boolean;
54
54
  getAccessToken(): string | null;
55
+ establishDeviceRefreshSlot(): Promise<number | null>;
55
56
  getAccessTokenExpiry(): number | null;
56
57
  waitForAuth(timeoutMs?: number): Promise<boolean>;
57
58
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -180,6 +180,7 @@ export declare function OxyServicesNodesMixin<T extends typeof OxyServicesBase>(
180
180
  getCurrentUserId(): string | null;
181
181
  hasValidToken(): boolean;
182
182
  getAccessToken(): string | null;
183
+ establishDeviceRefreshSlot(): Promise<number | null>;
183
184
  getAccessTokenExpiry(): number | null;
184
185
  waitForAuth(timeoutMs?: number): Promise<boolean>;
185
186
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -99,6 +99,7 @@ export declare function OxyServicesPaymentMixin<T extends typeof OxyServicesBase
99
99
  getCurrentUserId(): string | null;
100
100
  hasValidToken(): boolean;
101
101
  getAccessToken(): string | null;
102
+ establishDeviceRefreshSlot(): Promise<number | null>;
102
103
  getAccessTokenExpiry(): number | null;
103
104
  waitForAuth(timeoutMs?: number): Promise<boolean>;
104
105
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -128,6 +128,7 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
128
128
  getCurrentUserId(): string | null;
129
129
  hasValidToken(): boolean;
130
130
  getAccessToken(): string | null;
131
+ establishDeviceRefreshSlot(): Promise<number | null>;
131
132
  getAccessTokenExpiry(): number | null;
132
133
  waitForAuth(timeoutMs?: number): Promise<boolean>;
133
134
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -71,6 +71,7 @@ export declare function OxyServicesRedirectAuthMixin<T extends typeof OxyService
71
71
  getCurrentUserId(): string | null;
72
72
  hasValidToken(): boolean;
73
73
  getAccessToken(): string | null;
74
+ establishDeviceRefreshSlot(): Promise<number | null>;
74
75
  getAccessTokenExpiry(): number | null;
75
76
  waitForAuth(timeoutMs?: number): Promise<boolean>;
76
77
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -417,6 +417,7 @@ export declare function OxyServicesReputationMixin<T extends typeof OxyServicesB
417
417
  getCurrentUserId(): string | null;
418
418
  hasValidToken(): boolean;
419
419
  getAccessToken(): string | null;
420
+ establishDeviceRefreshSlot(): Promise<number | null>;
420
421
  getAccessTokenExpiry(): number | null;
421
422
  waitForAuth(timeoutMs?: number): Promise<boolean>;
422
423
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -66,6 +66,7 @@ export declare function OxyServicesSecurityMixin<T extends typeof OxyServicesBas
66
66
  getCurrentUserId(): string | null;
67
67
  hasValidToken(): boolean;
68
68
  getAccessToken(): string | null;
69
+ establishDeviceRefreshSlot(): Promise<number | null>;
69
70
  getAccessTokenExpiry(): number | null;
70
71
  waitForAuth(timeoutMs?: number): Promise<boolean>;
71
72
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -109,6 +109,7 @@ export declare function OxyServicesSilentAuthMixin<T extends typeof OxyServicesB
109
109
  getCurrentUserId(): string | null;
110
110
  hasValidToken(): boolean;
111
111
  getAccessToken(): string | null;
112
+ establishDeviceRefreshSlot(): Promise<number | null>;
112
113
  getAccessTokenExpiry(): number | null;
113
114
  waitForAuth(timeoutMs?: number): Promise<boolean>;
114
115
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -94,6 +94,7 @@ export declare function OxyServicesSsoMixin<T extends typeof OxyServicesBase>(Ba
94
94
  getCurrentUserId(): string | null;
95
95
  hasValidToken(): boolean;
96
96
  getAccessToken(): string | null;
97
+ establishDeviceRefreshSlot(): Promise<number | null>;
97
98
  getAccessTokenExpiry(): number | null;
98
99
  waitForAuth(timeoutMs?: number): Promise<boolean>;
99
100
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -92,6 +92,7 @@ export declare function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>
92
92
  getCurrentUserId(): string | null;
93
93
  hasValidToken(): boolean;
94
94
  getAccessToken(): string | null;
95
+ establishDeviceRefreshSlot(): Promise<number | null>;
95
96
  getAccessTokenExpiry(): number | null;
96
97
  waitForAuth(timeoutMs?: number): Promise<boolean>;
97
98
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -48,6 +48,17 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
48
48
  * server-to-server `/users/by-ids` bulk fetch with a bearer service token.
49
49
  */
50
50
  makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
51
+ /**
52
+ * Raw service credentials stored by `configureServiceAuth()` on the auth
53
+ * mixin (earlier in the pipeline). Surfaced here via `declare` — for the
54
+ * same typing reason as `makeServiceRequest` above — so `getUsersByIds` can
55
+ * detect whether this instance is service-configured (a backend) and pick
56
+ * the bearer-service path, or fall back to the user-session path (a browser/
57
+ * RN client). Both are `null` until `configureServiceAuth(apiKey, apiSecret)`
58
+ * is called.
59
+ */
60
+ _serviceApiKey: string | null;
61
+ _serviceApiSecret: string | null;
51
62
  /**
52
63
  * Get profile by username
53
64
  */
@@ -138,16 +149,28 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
138
149
  * by `id`); each is run through `normalizeUserIdentity`, matching
139
150
  * `getUserById`.
140
151
  *
141
- * **Service-token auth (required).** `/users/by-ids` is a server-to-server
142
- * bulk fetch of PUBLIC user data and is called via `makeServiceRequest`,
143
- * which attaches `Authorization: Bearer <serviceToken>`. oxy-api's CSRF
144
- * middleware skips bearer-authenticated requests, so the calling client
145
- * MUST be service-configured (`configureServiceAuth(apiKey, apiSecret)`)
146
- * before invoking this method; otherwise `getServiceToken()` throws because
147
- * no credentials are available. (A plain user-session request fails here:
148
- * server-to-server there is no cookie jar, so the auto-attached
149
- * `X-CSRF-Token` has no matching cookie and oxy-api rejects the POST with
150
- * 403 "CSRF token missing".)
152
+ * **Dual-mode auth.** `/users/by-ids` is `optionalUserOrServiceAuth` on
153
+ * oxy-api: it accepts a service token, a user session, or an anonymous
154
+ * caller, and returns the SAME public `{ data: PublicUserProfile[] }`
155
+ * payload (canonical `name.displayName` + `_count`) in every case — no
156
+ * viewer-specific fields. This method picks the path automatically:
157
+ * - **Service-configured host (backend):** when `configureServiceAuth(apiKey,
158
+ * apiSecret)` has been called, the chunk is fetched via `makeServiceRequest`
159
+ * (attaches `Authorization: Bearer <serviceToken>`). This is the
160
+ * server-to-server feed/notification hydration path (e.g. Mention's
161
+ * `PostHydrationService`) and is unchanged.
162
+ * - **Plain client (browser / React Native with a user session):** when no
163
+ * service credentials are configured, the chunk is fetched via
164
+ * `makeRequest`, which attaches the configured user bearer. oxy-api's CSRF
165
+ * middleware skips bearer-authenticated writes, and `makeRequest` only
166
+ * fetches a CSRF token for cookie-only (no-bearer) state-changing requests,
167
+ * so the user-bearer POST is sent without CSRF and succeeds. Previously
168
+ * this method always used the service path, so every client-side caller
169
+ * silently received `[]` because `getServiceToken()` had no credentials.
170
+ *
171
+ * Both paths run results through `normalizeUserIdentity` and unwrap the
172
+ * API's `{ data }` envelope identically (`makeServiceRequest` is literally
173
+ * `makeRequest` plus a bearer service header).
151
174
  *
152
175
  * Resilience: chunks are independent. A failed chunk is logged and skipped
153
176
  * — the method returns every user that resolved successfully rather than
@@ -363,6 +386,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
363
386
  getCurrentUserId(): string | null;
364
387
  hasValidToken(): boolean;
365
388
  getAccessToken(): string | null;
389
+ establishDeviceRefreshSlot(): Promise<number | null>;
366
390
  getAccessTokenExpiry(): number | null;
367
391
  waitForAuth(timeoutMs?: number): Promise<boolean>;
368
392
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -257,6 +257,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
257
257
  getCurrentUserId(): string | null;
258
258
  hasValidToken(): boolean;
259
259
  getAccessToken(): string | null;
260
+ establishDeviceRefreshSlot(): Promise<number | null>;
260
261
  getAccessTokenExpiry(): number | null;
261
262
  waitForAuth(timeoutMs?: number): Promise<boolean>;
262
263
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
@@ -465,6 +465,29 @@ export interface ServiceAssetMetadata {
465
465
  size: number;
466
466
  status: 'active' | 'trash';
467
467
  }
468
+ /**
469
+ * Reverse-lookup asset metadata returned by `POST /assets/service/by-sha256`.
470
+ *
471
+ * Resolves a content-addressed `sha256` digest back to the live Oxy asset that
472
+ * holds those bytes: its file `id`, MIME type, byte `size`, storage `status`,
473
+ * and — for active, public, CDN-reachable assets only — a public `url`
474
+ * (`cloud.oxy.so`). This is the inverse of {@link ServiceAssetMetadata}: it lets
475
+ * a `files:read`-scoped service-to-server caller (e.g. Mention's MTN materializer
476
+ * / node-blob sync) turn a record's `blob.sha256` into a servable asset.
477
+ *
478
+ * `url` is omitted for private/unlisted assets (and for public assets whose
479
+ * bytes are not yet CDN-reachable) — those must be streamed through the origin.
480
+ * Unknown or deleted hashes are omitted from the response (never error the whole
481
+ * batch), so the result may be shorter than the requested hash list.
482
+ */
483
+ export interface ServiceAssetMetadataBySha {
484
+ sha256: string;
485
+ id: string;
486
+ mime: string;
487
+ size: number;
488
+ status: 'active' | 'trash';
489
+ url?: string;
490
+ }
468
491
  /**
469
492
  * Account storage usage (server-side usage, not local AsyncStorage)
470
493
  */
@@ -94,6 +94,34 @@ export declare function ssoAttemptedKey(origin: string): string;
94
94
  * IdP, while a truly first-time anonymous visitor is never force-bounced.
95
95
  */
96
96
  export declare function ssoPriorSessionKey(origin: string): string;
97
+ /**
98
+ * Per-origin DURABLE "the user DELIBERATELY signed out on this device/origin"
99
+ * flag.
100
+ *
101
+ * Like {@link ssoPriorSessionKey} this lives in DURABLE storage (web
102
+ * `localStorage`; services uses its own `storageKeyPrefix`-scoped key), NOT the
103
+ * per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
104
+ *
105
+ * It exists purely to suppress AUTOMATIC silent restore after a deliberate
106
+ * sign-out: a still-live IdP session (the central `fedcm_session` / the FedCM
107
+ * credential association) would otherwise let `fedcm-silent` / the per-apex
108
+ * `/auth/silent` iframe re-mint a session on the very next cold boot, so a user
109
+ * who pressed "Sign out" gets silently signed back in on reload. With this flag
110
+ * set, those silent cold-boot steps are skipped while the Gmail-style
111
+ * returning-account fast-path is otherwise preserved.
112
+ *
113
+ * Lifecycle (mirrors the existing gate machinery — set on a definitive event,
114
+ * cleared on its inverse):
115
+ * - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
116
+ * and the SSO bounce state).
117
+ * - CLEARED on ANY deliberate sign-in (password, FedCM, account switch, device
118
+ * claim) so a real sign-in fully re-enables silent restore — there is no
119
+ * "stuck signed out" state.
120
+ *
121
+ * NOTE: this gates only AUTOMATIC/silent restore. An INTERACTIVE sign-in always
122
+ * clears it first, so the user can always sign back in.
123
+ */
124
+ export declare function ssoSignedOutKey(origin: string): string;
97
125
  /**
98
126
  * Per-origin marker written by the pre-hydration callback bootstrap.
99
127
  *
@@ -174,6 +202,25 @@ export declare function isCentralIdPOrigin(origin: string): boolean;
174
202
  * `Date.now()`.
175
203
  */
176
204
  export declare function guardActive(storage: Pick<Storage, 'getItem'>, origin: string, now?: number): boolean;
205
+ /**
206
+ * Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
207
+ * user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
208
+ *
209
+ * When `true`, the silent cold-boot steps that can re-mint a session from a
210
+ * still-live IdP session WITHOUT user intent — `fedcm-silent` and the per-apex
211
+ * `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
212
+ * not silently signed back in on the next reload. Interactive sign-in clears the
213
+ * flag, so this never blocks a deliberate re-sign-in.
214
+ *
215
+ * Defensive: a `getItem` that throws (locked/disabled storage) is treated as NOT
216
+ * suppressed, so the gate fails toward the normal restore behaviour rather than
217
+ * wedging the user out.
218
+ *
219
+ * @param storage - The DURABLE storage to read (web `localStorage`; injected for
220
+ * testability).
221
+ * @param origin - The page origin whose signed-out flag to evaluate.
222
+ */
223
+ export declare function silentRestoreSuppressed(storage: Pick<Storage, 'getItem'>, origin: string): boolean;
177
224
  /**
178
225
  * Inputs to the smart {@link allowSsoBounce} gate.
179
226
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
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",
@@ -509,11 +509,35 @@ export class AuthManager {
509
509
 
510
510
  this.currentAuthMethod = method;
511
511
 
512
- const decodedAuthuser = session.accessToken
513
- ? AuthManager.decodeAuthuserFromAccessToken(session.accessToken)
512
+ // Register this primary session in the device's first-party multi-account
513
+ // refresh-cookie set (web only). Every web primary commit funnels through
514
+ // here (FedCM, central `/sso` return, credentials), but only a same-apex
515
+ // `/fedcm/exchange` plants an `oxy_rt_<authuser>` slot as a side effect — the
516
+ // cross-origin/credential-less restores (`/sso/exchange`, the IdP
517
+ // `/auth/silent` postMessage) cannot set an `api.oxy.so` cookie. WITHOUT this
518
+ // call those primaries never join the device set, so `refresh-all` returns no
519
+ // accounts and account-switch persistence has no foundation. The shared
520
+ // `POST /auth/session` primitive plants/reuses this user's slot where the
521
+ // cookies ARE visible, re-plants the rotated access token, and returns the
522
+ // AUTHORITATIVE `authuser` slot (the one actually written) — preferred over the
523
+ // JWT-decoded guess, which may not correspond to a real cookie. No-op on native
524
+ // (returns `null`); best-effort on transient failure.
525
+ const establishedAuthuser = await this.oxyServices.establishDeviceRefreshSlot();
526
+ // The slot mint rotated the access token; pick it up so the registry and the
527
+ // refresh authority track the cookie just written. Falls back to the original
528
+ // session token when the slot was a no-op (native) or failed.
529
+ const rotatedToken =
530
+ (establishedAuthuser !== null ? this.oxyServices.getAccessToken() : null) ??
531
+ session.accessToken;
532
+ if (rotatedToken) {
533
+ this._lastKnownAccessToken = rotatedToken;
534
+ }
535
+
536
+ const decodedAuthuser = rotatedToken
537
+ ? AuthManager.decodeAuthuserFromAccessToken(rotatedToken)
514
538
  : null;
515
- const authuser = decodedAuthuser ?? 0;
516
- if (session.accessToken && session.sessionId) {
539
+ const authuser = establishedAuthuser ?? decodedAuthuser ?? 0;
540
+ if (rotatedToken && session.sessionId) {
517
541
  this.accounts.set(authuser, {
518
542
  authuser,
519
543
  sessionId: session.sessionId,
@@ -523,7 +547,7 @@ export class AuthManager {
523
547
  name: session.user.name,
524
548
  avatar: session.user.avatar ?? null,
525
549
  },
526
- accessToken: session.accessToken,
550
+ accessToken: rotatedToken,
527
551
  expiresAt: session.expiresAt,
528
552
  });
529
553
  this.activeAuthuser = authuser;