@oxy.so/core 1.12.0 → 1.12.1
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 +15 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.user.js +15 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +11 -2
- package/package.json +1 -1
- package/src/mixins/OxyServices.user.ts +23 -6
- package/src/mixins/__tests__/getUsersByIds.test.ts +27 -0
|
@@ -97,6 +97,13 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
97
97
|
*/
|
|
98
98
|
_serviceApiKey: string | null;
|
|
99
99
|
_serviceApiSecret: string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Whether this process can mint a service token by attesting what it is
|
|
102
|
+
* (ADR 0026), defined on the auth mixin. A host with no key pair that CAN
|
|
103
|
+
* attest is a first-party backend exactly like one with a key pair, and
|
|
104
|
+
* `getUsersByIds` must treat it as one.
|
|
105
|
+
*/
|
|
106
|
+
_canUseWorkloadIdentity: () => Promise<boolean>;
|
|
100
107
|
/**
|
|
101
108
|
* Get profile by username.
|
|
102
109
|
*
|
|
@@ -229,8 +236,10 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
229
236
|
* caller, and returns the SAME public `{ data: PublicUserProfile[] }`
|
|
230
237
|
* payload (canonical `name.displayName` + `_count`) in every case — no
|
|
231
238
|
* viewer-specific fields. This method picks the path automatically:
|
|
232
|
-
* - **Service
|
|
233
|
-
* apiSecret)` has been called, the
|
|
239
|
+
* - **Service host (backend):** when `configureServiceAuth(apiKey,
|
|
240
|
+
* apiSecret)` has been called, OR the process can attest its workload
|
|
241
|
+
* identity (ADR 0026 — an ECS task with no key pair), the chunk is fetched
|
|
242
|
+
* via `makeServiceRequest`
|
|
234
243
|
* (attaches `Authorization: Bearer <serviceToken>`). This is the
|
|
235
244
|
* server-to-server feed/notification hydration path (e.g. Mention's
|
|
236
245
|
* `PostHydrationService`) and is unchanged.
|
package/package.json
CHANGED
|
@@ -155,6 +155,14 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
155
155
|
declare _serviceApiKey: string | null;
|
|
156
156
|
declare _serviceApiSecret: string | null;
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether this process can mint a service token by attesting what it is
|
|
160
|
+
* (ADR 0026), defined on the auth mixin. A host with no key pair that CAN
|
|
161
|
+
* attest is a first-party backend exactly like one with a key pair, and
|
|
162
|
+
* `getUsersByIds` must treat it as one.
|
|
163
|
+
*/
|
|
164
|
+
declare _canUseWorkloadIdentity: () => Promise<boolean>;
|
|
165
|
+
|
|
158
166
|
/**
|
|
159
167
|
* Get profile by username.
|
|
160
168
|
*
|
|
@@ -464,8 +472,10 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
464
472
|
* caller, and returns the SAME public `{ data: PublicUserProfile[] }`
|
|
465
473
|
* payload (canonical `name.displayName` + `_count`) in every case — no
|
|
466
474
|
* viewer-specific fields. This method picks the path automatically:
|
|
467
|
-
* - **Service
|
|
468
|
-
* apiSecret)` has been called, the
|
|
475
|
+
* - **Service host (backend):** when `configureServiceAuth(apiKey,
|
|
476
|
+
* apiSecret)` has been called, OR the process can attest its workload
|
|
477
|
+
* identity (ADR 0026 — an ECS task with no key pair), the chunk is fetched
|
|
478
|
+
* via `makeServiceRequest`
|
|
469
479
|
* (attaches `Authorization: Bearer <serviceToken>`). This is the
|
|
470
480
|
* server-to-server feed/notification hydration path (e.g. Mention's
|
|
471
481
|
* `PostHydrationService`) and is unchanged.
|
|
@@ -500,10 +510,17 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
500
510
|
chunks.push(uniqueIds.slice(i, i + USERS_BY_IDS_CHUNK_SIZE));
|
|
501
511
|
}
|
|
502
512
|
|
|
503
|
-
// A backend
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
513
|
+
// A backend uses the bearer-service path; any other caller (browser / RN
|
|
514
|
+
// with a user session) uses the user-bearer path. "A backend" means one
|
|
515
|
+
// that can get a service token at all — a key pair, OR a task role it can
|
|
516
|
+
// attest. Asking only about the key pair sent every attested backend down
|
|
517
|
+
// the user path with no bearer: an anonymous request, charged to the
|
|
518
|
+
// shared NAT address's per-IP budget and slowed by its 500ms penalty.
|
|
519
|
+
// Measured from Mention's task (2026-09-25): 520ms per anonymous chunk
|
|
520
|
+
// against 20ms with the service token, enough to put its 1.5s hydration
|
|
521
|
+
// deadline out of reach on every cache miss.
|
|
522
|
+
const useServiceAuth = Boolean(this._serviceApiKey && this._serviceApiSecret)
|
|
523
|
+
|| await this._canUseWorkloadIdentity();
|
|
507
524
|
|
|
508
525
|
// Run chunks concurrently; a single chunk failure must not sink the rest.
|
|
509
526
|
const settled = await Promise.all(
|
|
@@ -110,6 +110,33 @@ describe('OxyServices.getUsersByIds — dual-mode auth', () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
describe('with an attestable workload identity and no key pair (ADR 0026 backend)', () => {
|
|
114
|
+
it('uses makeServiceRequest — an attested backend is still a backend', async () => {
|
|
115
|
+
jest
|
|
116
|
+
.spyOn(oxy as unknown as { _canUseWorkloadIdentity: () => Promise<boolean> }, '_canUseWorkloadIdentity')
|
|
117
|
+
.mockResolvedValue(true);
|
|
118
|
+
makeServiceRequestSpy.mockResolvedValueOnce([makeRawUser('a')]);
|
|
119
|
+
|
|
120
|
+
const result = await oxy.getUsersByIds(['a']);
|
|
121
|
+
|
|
122
|
+
expect(makeRequestSpy).not.toHaveBeenCalled();
|
|
123
|
+
expect(makeServiceRequestSpy).toHaveBeenCalledWith('POST', '/users/by-ids', { ids: ['a'] });
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('stays on the user path when the host cannot attest', async () => {
|
|
128
|
+
jest
|
|
129
|
+
.spyOn(oxy as unknown as { _canUseWorkloadIdentity: () => Promise<boolean> }, '_canUseWorkloadIdentity')
|
|
130
|
+
.mockResolvedValue(false);
|
|
131
|
+
makeRequestSpy.mockResolvedValueOnce([makeRawUser('a')]);
|
|
132
|
+
|
|
133
|
+
await oxy.getUsersByIds(['a']);
|
|
134
|
+
|
|
135
|
+
expect(makeServiceRequestSpy).not.toHaveBeenCalled();
|
|
136
|
+
expect(makeRequestSpy).toHaveBeenCalledWith('POST', '/users/by-ids', { ids: ['a'] }, { cache: false });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
113
140
|
describe('chunking + resilience (applies to both modes)', () => {
|
|
114
141
|
it('chunks at 100 ids per request on the user path and flattens results', async () => {
|
|
115
142
|
const ids = Array.from({ length: 250 }, (_, i) => `id-${i}`);
|