@oxyhq/core 12.4.0 → 12.5.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/mixins/OxyServices.user.js +11 -0
- package/dist/cjs/server/safeFetch.js +4 -4
- package/dist/cjs/session/refresh.js +14 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.user.js +11 -0
- package/dist/esm/server/safeFetch.js +4 -4
- package/dist/esm/session/refresh.js +14 -3
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +10 -1
- package/dist/types/models/interfaces.d.ts +15 -1
- package/dist/types/server/safeFetch.d.ts +2 -0
- package/package.json +2 -2
- package/src/mixins/OxyServices.user.ts +15 -0
- package/src/models/interfaces.ts +20 -1
- package/src/server/safeFetch.ts +6 -1
- package/src/session/__tests__/refresh.test.ts +20 -0
- package/src/session/refresh.ts +12 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
4
|
import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
|
|
5
|
-
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem } from '@oxyhq/contracts';
|
|
5
|
+
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem, ThemePreference } from '@oxyhq/contracts';
|
|
6
6
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
7
7
|
import { type PaginationParams } from '../utils/apiUtils';
|
|
8
8
|
/**
|
|
@@ -292,6 +292,15 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
292
292
|
* `PUT /users/me` — same cache-invalidation behaviour as `updateProfile`.
|
|
293
293
|
*/
|
|
294
294
|
updateUserPreferences(preferences: Partial<UserPreferences>): Promise<User>;
|
|
295
|
+
/**
|
|
296
|
+
* Update the authenticated user's portable theme preference (light/dark/
|
|
297
|
+
* system + Bloom color-preset key). Persisted on the User document via the
|
|
298
|
+
* SAME `PUT /users/me` settings path as the other preferences — same cache
|
|
299
|
+
* invalidation — so the next cold boot serves it on the self/session payload
|
|
300
|
+
* with no extra network call. The full object is written (both `mode` and
|
|
301
|
+
* `colorPreset` are required by the API).
|
|
302
|
+
*/
|
|
303
|
+
updateThemePreference(themePreference: ThemePreference): Promise<User>;
|
|
295
304
|
/**
|
|
296
305
|
* Request account verification
|
|
297
306
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrganizationCategory, UserNameResponse } from '@oxyhq/contracts';
|
|
1
|
+
import type { OrganizationCategory, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
|
|
2
2
|
export interface OxyConfig {
|
|
3
3
|
baseURL: string;
|
|
4
4
|
cloudURL?: string;
|
|
@@ -147,6 +147,20 @@ export interface User {
|
|
|
147
147
|
languages?: string[];
|
|
148
148
|
notificationPreferences?: NotificationPreferences;
|
|
149
149
|
userPreferences?: UserPreferences;
|
|
150
|
+
/**
|
|
151
|
+
* Portable theme preference (mode + Bloom color-preset key). Rides the
|
|
152
|
+
* self/session payload so it is present on the current-user DTO at cold boot
|
|
153
|
+
* (`useAuth().user.themePreference`) with no extra fetch. Absent until the
|
|
154
|
+
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
155
|
+
*/
|
|
156
|
+
themePreference?: ThemePreference;
|
|
157
|
+
/**
|
|
158
|
+
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
159
|
+
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
160
|
+
* request is authenticated; absent for anonymous requests, self-views, and
|
|
161
|
+
* bulk fetches — so `undefined` means "unknown", not "not following".
|
|
162
|
+
*/
|
|
163
|
+
relationship?: UserRelationship;
|
|
150
164
|
[key: string]: unknown;
|
|
151
165
|
}
|
|
152
166
|
/**
|
|
@@ -91,6 +91,8 @@ export interface SafeFetchOptions {
|
|
|
91
91
|
method?: string;
|
|
92
92
|
/** Extra request headers. A `User-Agent` is added if none is provided. */
|
|
93
93
|
headers?: Record<string, string>;
|
|
94
|
+
/** Request body for POST/PUT/PATCH. */
|
|
95
|
+
body?: string | Buffer;
|
|
94
96
|
/**
|
|
95
97
|
* Maximum number of redirects to follow (each re-validated). Defaults to
|
|
96
98
|
* {@link MAX_REDIRECTS}. Set to `0` to disallow redirects.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.5.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",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@noble/ciphers": "^1.3.0",
|
|
113
113
|
"@noble/hashes": "^1.8.0",
|
|
114
|
-
"@oxyhq/contracts": "^0.
|
|
114
|
+
"@oxyhq/contracts": "^0.17.0",
|
|
115
115
|
"@oxyhq/protocol": "^0.1.5",
|
|
116
116
|
"bip39": "^3.1.0",
|
|
117
117
|
"buffer": "^6.0.3",
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
UserProfileUpdate,
|
|
16
16
|
RecommendationRequest,
|
|
17
17
|
RecommendationItem,
|
|
18
|
+
ThemePreference,
|
|
18
19
|
} from '@oxyhq/contracts';
|
|
19
20
|
import { recommendationRequestSchema } from '@oxyhq/contracts';
|
|
20
21
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
@@ -628,6 +629,20 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
628
629
|
return this.updateProfile({ userPreferences: preferences });
|
|
629
630
|
}
|
|
630
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Update the authenticated user's portable theme preference (light/dark/
|
|
634
|
+
* system + Bloom color-preset key). Persisted on the User document via the
|
|
635
|
+
* SAME `PUT /users/me` settings path as the other preferences — same cache
|
|
636
|
+
* invalidation — so the next cold boot serves it on the self/session payload
|
|
637
|
+
* with no extra network call. The full object is written (both `mode` and
|
|
638
|
+
* `colorPreset` are required by the API).
|
|
639
|
+
*/
|
|
640
|
+
async updateThemePreference(
|
|
641
|
+
themePreference: ThemePreference
|
|
642
|
+
): Promise<User> {
|
|
643
|
+
return this.updateProfile({ themePreference });
|
|
644
|
+
}
|
|
645
|
+
|
|
631
646
|
/**
|
|
632
647
|
* Request account verification
|
|
633
648
|
*/
|
package/src/models/interfaces.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
OrganizationCategory,
|
|
3
|
+
UserNameResponse,
|
|
4
|
+
UserRelationship,
|
|
5
|
+
ThemePreference,
|
|
6
|
+
} from '@oxyhq/contracts';
|
|
2
7
|
|
|
3
8
|
export interface OxyConfig {
|
|
4
9
|
baseURL: string;
|
|
@@ -163,6 +168,20 @@ export interface User {
|
|
|
163
168
|
notificationPreferences?: NotificationPreferences;
|
|
164
169
|
// General app-wide user preferences. Updated via `PUT /users/me`.
|
|
165
170
|
userPreferences?: UserPreferences;
|
|
171
|
+
/**
|
|
172
|
+
* Portable theme preference (mode + Bloom color-preset key). Rides the
|
|
173
|
+
* self/session payload so it is present on the current-user DTO at cold boot
|
|
174
|
+
* (`useAuth().user.themePreference`) with no extra fetch. Absent until the
|
|
175
|
+
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
176
|
+
*/
|
|
177
|
+
themePreference?: ThemePreference;
|
|
178
|
+
/**
|
|
179
|
+
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
180
|
+
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
181
|
+
* request is authenticated; absent for anonymous requests, self-views, and
|
|
182
|
+
* bulk fetches — so `undefined` means "unknown", not "not following".
|
|
183
|
+
*/
|
|
184
|
+
relationship?: UserRelationship;
|
|
166
185
|
[key: string]: unknown;
|
|
167
186
|
}
|
|
168
187
|
|
package/src/server/safeFetch.ts
CHANGED
|
@@ -390,6 +390,8 @@ export interface SafeFetchOptions {
|
|
|
390
390
|
method?: string;
|
|
391
391
|
/** Extra request headers. A `User-Agent` is added if none is provided. */
|
|
392
392
|
headers?: Record<string, string>;
|
|
393
|
+
/** Request body for POST/PUT/PATCH. */
|
|
394
|
+
body?: string | Buffer;
|
|
393
395
|
/**
|
|
394
396
|
* Maximum number of redirects to follow (each re-validated). Defaults to
|
|
395
397
|
* {@link MAX_REDIRECTS}. Set to `0` to disallow redirects.
|
|
@@ -475,6 +477,7 @@ function fetchOnce(
|
|
|
475
477
|
options: https.RequestOptions,
|
|
476
478
|
isHttps: boolean,
|
|
477
479
|
headersTimeoutMs: number,
|
|
480
|
+
body?: string | Buffer,
|
|
478
481
|
): Promise<IncomingMessage> {
|
|
479
482
|
return new Promise<IncomingMessage>((resolve, reject) => {
|
|
480
483
|
const transport = isHttps ? https : http;
|
|
@@ -484,7 +487,7 @@ function fetchOnce(
|
|
|
484
487
|
req.destroy(new UpstreamError('upstream headers timeout'));
|
|
485
488
|
});
|
|
486
489
|
req.on('error', (err) => reject(err));
|
|
487
|
-
req.end();
|
|
490
|
+
req.end(body);
|
|
488
491
|
});
|
|
489
492
|
}
|
|
490
493
|
|
|
@@ -506,6 +509,7 @@ export async function safeFetch(
|
|
|
506
509
|
const {
|
|
507
510
|
method = 'GET',
|
|
508
511
|
headers: callerHeaders,
|
|
512
|
+
body,
|
|
509
513
|
maxRedirects = MAX_REDIRECTS,
|
|
510
514
|
headersTimeoutMs = UPSTREAM_HEADERS_TIMEOUT_MS,
|
|
511
515
|
signal,
|
|
@@ -548,6 +552,7 @@ export async function safeFetch(
|
|
|
548
552
|
requestOptions,
|
|
549
553
|
target.protocol === 'https:',
|
|
550
554
|
headersTimeoutMs,
|
|
555
|
+
body,
|
|
551
556
|
);
|
|
552
557
|
|
|
553
558
|
const status = response.statusCode ?? 0;
|
|
@@ -159,6 +159,26 @@ describe('refreshPersistedSession — arm 1 (device-secret mint)', () => {
|
|
|
159
159
|
expect(await store.load()).toEqual(STORED);
|
|
160
160
|
expect(signInWithSharedIdentity).not.toHaveBeenCalled();
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it('KEEPS the store on an UNRECOGNIZED 401 (proxy / middleware / deploy-window) — never wipes the credential on an ambiguous 401', async () => {
|
|
164
|
+
const store = createMemoryAuthStateStore();
|
|
165
|
+
await store.save(STORED);
|
|
166
|
+
const signInWithSharedIdentity = jest.fn(async () => null);
|
|
167
|
+
const { oxy } = makeOxy({
|
|
168
|
+
mintFromDeviceSecret: async () => {
|
|
169
|
+
// A 401 whose body is NEITHER `invalid_device_secret` NOR `no_active_session`
|
|
170
|
+
// — an auth-layer / proxy / starting-instance 401 common during a deploy
|
|
171
|
+
// window. It is NOT proof the secret diverged, so the durable device
|
|
172
|
+
// credential must survive (treated as transient) and a later attempt self-heals.
|
|
173
|
+
throw Object.assign(new Error('Unauthorized'), { status: 401 });
|
|
174
|
+
},
|
|
175
|
+
signInWithSharedIdentity,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(await refreshPersistedSession({ oxy, store, allowSharedKeyFallback: true })).toBeNull();
|
|
179
|
+
expect(await store.load()).toEqual(STORED);
|
|
180
|
+
expect(signInWithSharedIdentity).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
162
182
|
});
|
|
163
183
|
|
|
164
184
|
describe('refreshPersistedSession — arm 2 (native shared-key fallback)', () => {
|
package/src/session/refresh.ts
CHANGED
|
@@ -136,9 +136,18 @@ export async function refreshDeviceSecretArm(deps: {
|
|
|
136
136
|
// Structural read (not `instanceof Error`): the thrown value can be a
|
|
137
137
|
// plain ApiError-shaped object or come from another realm.
|
|
138
138
|
const message = (error as { message?: unknown })?.message;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const body = typeof message === 'string' ? message : '';
|
|
140
|
+
// ONLY the server's explicit `invalid_device_secret` proves the presented
|
|
141
|
+
// secret is bad and may clear the durable device credential. `no_active_session`
|
|
142
|
+
// is an authoritative signed-out. ANY OTHER 401 — a middleware/CSRF/proxy 401,
|
|
143
|
+
// an ALB/starting-instance 401, a CORS error page, etc., all common during a
|
|
144
|
+
// deploy/restart window — is NOT proof the secret diverged: treat it as
|
|
145
|
+
// transient and KEEP the credential so a later attempt self-heals. Wiping the
|
|
146
|
+
// credential on an ambiguous 401 is what logged users out on every deploy,
|
|
147
|
+
// ecosystem-wide.
|
|
148
|
+
if (body.includes('invalid_device_secret')) return { status: 'invalid-secret' };
|
|
149
|
+
if (body.includes('no_active_session')) return { status: 'no-session' };
|
|
150
|
+
return { status: 'transient' };
|
|
142
151
|
}
|
|
143
152
|
return { status: 'transient' };
|
|
144
153
|
}
|