@oxyhq/core 21.0.0 → 21.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +47 -8
- package/dist/cjs/i18n/locales/en-US.json +7 -2
- package/dist/cjs/i18n/locales/es-ES.json +7 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +7 -2
- package/dist/cjs/i18n/locales/locales/es-ES.json +7 -2
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/inference/OxyInferenceClient.js +330 -0
- package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
- package/dist/cjs/mixins/OxyServices.inference.js +59 -0
- package/dist/cjs/mixins/OxyServices.utility.js +18 -6
- package/dist/cjs/mixins/index.js +6 -0
- package/dist/cjs/server/auth.js +76 -0
- package/dist/cjs/server/cors.js +84 -15
- package/dist/cjs/server/index.js +5 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +47 -8
- package/dist/esm/i18n/locales/en-US.json +7 -2
- package/dist/esm/i18n/locales/es-ES.json +7 -2
- package/dist/esm/i18n/locales/locales/en-US.json +7 -2
- package/dist/esm/i18n/locales/locales/es-ES.json +7 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/inference/OxyInferenceClient.js +325 -0
- package/dist/esm/mixins/OxyServices.accounts.js +5 -72
- package/dist/esm/mixins/OxyServices.inference.js +56 -0
- package/dist/esm/mixins/OxyServices.utility.js +18 -6
- package/dist/esm/mixins/index.js +6 -0
- package/dist/esm/server/auth.js +72 -0
- package/dist/esm/server/cors.js +82 -15
- package/dist/esm/server/index.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +39 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
- package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
- package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/server/auth.d.ts +80 -0
- package/dist/types/server/cors.d.ts +41 -0
- package/dist/types/server/index.d.ts +2 -2
- package/package.json +2 -2
- package/src/HttpService.ts +50 -10
- package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
- package/src/i18n/locales/en-US.json +7 -2
- package/src/i18n/locales/es-ES.json +7 -2
- package/src/index.ts +19 -7
- package/src/inference/OxyInferenceClient.ts +590 -0
- package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
- package/src/mixins/OxyServices.accounts.ts +75 -176
- package/src/mixins/OxyServices.inference.ts +57 -0
- package/src/mixins/OxyServices.utility.ts +58 -14
- package/src/mixins/__tests__/accounts.test.ts +57 -102
- package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
- package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
- package/src/mixins/index.ts +8 -0
- package/src/server/__tests__/cors.socket.test.ts +225 -0
- package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
- package/src/server/auth.ts +118 -0
- package/src/server/cors.ts +87 -12
- package/src/server/index.ts +6 -0
- package/src/session/__tests__/accountDialogShape.test.ts +118 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inference API, reached with whatever bearer this session already holds
|
|
3
|
+
* (issue #972, workstream 15).
|
|
4
|
+
*
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const models = await oxyServices.inference().listModels();
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* One method, and it is a FACTORY rather than a set of inference methods on
|
|
10
|
+
* `OxyServices`. The calls themselves live once, in
|
|
11
|
+
* {@link OxyInferenceClient} — which an external developer holding only an
|
|
12
|
+
* `oxy_sk_…` machine key constructs directly, with no Oxy session anywhere in
|
|
13
|
+
* the picture. Declaring the same calls a second time here would give the
|
|
14
|
+
* ecosystem two spellings of one request, and only one of them would stay
|
|
15
|
+
* correct.
|
|
16
|
+
*
|
|
17
|
+
* This is the reasoning `createLinkedClient` is already built on: the plumbing
|
|
18
|
+
* that binds an Oxy bearer to a client belongs in core, once, rather than in
|
|
19
|
+
* each app.
|
|
20
|
+
*
|
|
21
|
+
* The credential is a FUNCTION, not the current token: a session bearer rotates
|
|
22
|
+
* on refresh and on account switch, and a client that captured one at
|
|
23
|
+
* construction would start answering 401 an hour into the process's life.
|
|
24
|
+
*/
|
|
25
|
+
import { OxyInferenceClient } from '../inference/OxyInferenceClient';
|
|
26
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
27
|
+
export declare function OxyServicesInferenceMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
28
|
+
new (...args: any[]): {
|
|
29
|
+
/** @internal Memoized so repeated calls return one object identity. */
|
|
30
|
+
_inferenceClient: OxyInferenceClient | null;
|
|
31
|
+
/**
|
|
32
|
+
* The inference client for this session.
|
|
33
|
+
*
|
|
34
|
+
* Bound to this instance's base URL and to `getAccessToken()`, so it
|
|
35
|
+
* follows every refresh, sign-in and account switch without being
|
|
36
|
+
* rebuilt.
|
|
37
|
+
*
|
|
38
|
+
* A service-authenticated process wants a different credential and
|
|
39
|
+
* builds {@link OxyInferenceClient} directly:
|
|
40
|
+
* `new OxyInferenceClient({ credential: () => oxy.getServiceToken() })`.
|
|
41
|
+
* The mint is asynchronous and cached, which is exactly what a
|
|
42
|
+
* credential function is for.
|
|
43
|
+
*/
|
|
44
|
+
inference(): OxyInferenceClient;
|
|
45
|
+
httpService: import("../HttpService").HttpService;
|
|
46
|
+
cloudURL: string;
|
|
47
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
48
|
+
__resetTokensForTests(): void;
|
|
49
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
50
|
+
getBaseURL(): string;
|
|
51
|
+
getClient(): import("../HttpService").HttpService;
|
|
52
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
53
|
+
getMetrics(): {
|
|
54
|
+
totalRequests: number;
|
|
55
|
+
successfulRequests: number;
|
|
56
|
+
failedRequests: number;
|
|
57
|
+
cacheHits: number;
|
|
58
|
+
cacheMisses: number;
|
|
59
|
+
averageResponseTime: number;
|
|
60
|
+
};
|
|
61
|
+
clearCache(): void;
|
|
62
|
+
clearCacheEntry(key: string): void;
|
|
63
|
+
clearCacheByPrefix(prefix: string): number;
|
|
64
|
+
getCacheStats(): {
|
|
65
|
+
size: number;
|
|
66
|
+
hits: number;
|
|
67
|
+
misses: number;
|
|
68
|
+
hitRate: number;
|
|
69
|
+
};
|
|
70
|
+
getCloudURL(): string;
|
|
71
|
+
setTokens(accessToken: string): void;
|
|
72
|
+
clearTokens(): void;
|
|
73
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
74
|
+
_cachedUserId: string | null | undefined;
|
|
75
|
+
_cachedAccessToken: string | null;
|
|
76
|
+
getCurrentUserId(): string | null;
|
|
77
|
+
hasValidToken(): boolean;
|
|
78
|
+
getAccessToken(): string | null;
|
|
79
|
+
getAccessTokenExpiry(): number | null;
|
|
80
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
81
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
82
|
+
maxRetries?: number;
|
|
83
|
+
retryDelay?: number;
|
|
84
|
+
authTimeoutMs?: number;
|
|
85
|
+
}): Promise<T_1>;
|
|
86
|
+
validate(): Promise<boolean>;
|
|
87
|
+
handleError(error: unknown): Error;
|
|
88
|
+
healthCheck(): Promise<{
|
|
89
|
+
status: string;
|
|
90
|
+
users?: number;
|
|
91
|
+
timestamp?: string;
|
|
92
|
+
[key: string]: any;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
} & T;
|
|
@@ -19,9 +19,21 @@ export interface ServiceActingAsVerification {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Service app metadata attached to requests authenticated with service tokens.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* Every field comes from the token's SIGNED payload and is populated only after
|
|
24
|
+
* the signature, `iss`/`aud`/`type` binding and expiry all pass — so a verifier
|
|
25
|
+
* holding this object can name the responsible principals without a lookup of
|
|
26
|
+
* its own. Together with `credentialId` and `ownerAccountId` it is the canonical
|
|
27
|
+
* attribution tuple of ADR 0007 minus the delegated user.
|
|
28
|
+
*
|
|
29
|
+
* `scopes` are the EFFECTIVE scopes: the credential's own scopes intersected
|
|
30
|
+
* with the owning application's grant at mint time (the API's `intersectScopes`
|
|
31
|
+
* is the single authority for that intersection — nothing re-intersects here).
|
|
32
|
+
* Route-level checks narrow further via `requireScope()`.
|
|
33
|
+
*
|
|
34
|
+
* A delegated end user is NOT a field of this type, and must never become one.
|
|
35
|
+
* It lives in `req.serviceActingAs` / `req.userId`, is authorised per request,
|
|
36
|
+
* and is attribution only.
|
|
25
37
|
*/
|
|
26
38
|
export interface ServiceApp {
|
|
27
39
|
appId: string;
|
|
@@ -29,6 +41,12 @@ export interface ServiceApp {
|
|
|
29
41
|
scopes: string[];
|
|
30
42
|
/** The credentialId of the specific service credential that minted this token. */
|
|
31
43
|
credentialId: string;
|
|
44
|
+
/**
|
|
45
|
+
* The Oxy account that owns `appId` and is financially responsible for it.
|
|
46
|
+
* The BILLING principal — never a user id, and never the delegated
|
|
47
|
+
* `X-Oxy-User-Id` (ADR 0007).
|
|
48
|
+
*/
|
|
49
|
+
ownerAccountId: string;
|
|
32
50
|
/** Test/live isolation (F2.0): which `ApplicationCredential.environment` minted this token. */
|
|
33
51
|
environment: OxyServiceEnvironment;
|
|
34
52
|
}
|
|
@@ -49,11 +67,24 @@ interface AuthMiddlewareOptions {
|
|
|
49
67
|
* When provided, service tokens will be cryptographically verified.
|
|
50
68
|
* When omitted, service tokens will be rejected (secure default).
|
|
51
69
|
*
|
|
52
|
-
* **
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
70
|
+
* **The only value that works is `ACCESS_TOKEN_SECRET`, and you should not
|
|
71
|
+
* want to hold it — see issue #987 and ADR 0012.** The Oxy API signs service
|
|
72
|
+
* tokens with `ACCESS_TOKEN_SECRET` (`packages/api/src/routes/auth.ts`),
|
|
73
|
+
* which is also the key that signs every user access token. There is no
|
|
74
|
+
* separate service-token secret: earlier revisions of this comment named a
|
|
75
|
+
* `SERVICE_TOKEN_SECRET` that has never existed in the API, in any workflow or
|
|
76
|
+
* in any task definition, and passing one would fail every verification.
|
|
77
|
+
*
|
|
78
|
+
* The consequence to hold onto: the scheme is symmetric, so a host that can
|
|
79
|
+
* VERIFY a service token can also MINT one — including a user access token.
|
|
80
|
+
* **Local verification is therefore appropriate only inside the Oxy API's own
|
|
81
|
+
* trust boundary, and no service outside it holds this key today.** Do not be
|
|
82
|
+
* the first: if you need to verify Oxy service tokens from another service,
|
|
83
|
+
* follow #987 rather than copying the secret.
|
|
84
|
+
*
|
|
85
|
+
* `docs/adr/0012-service-token-signing-key-model.md` records the decision to
|
|
86
|
+
* retire this option in favour of asymmetric signing against a published
|
|
87
|
+
* JWKS, at which point it is removed rather than deprecated.
|
|
57
88
|
*/
|
|
58
89
|
jwtSecret?: string;
|
|
59
90
|
/**
|
|
@@ -125,7 +156,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
125
156
|
* additionally checked for `aud`, `iss`, and `type` claims to prevent
|
|
126
157
|
* cross-token-type confusion attacks.
|
|
127
158
|
* - The backend's own `authMiddleware` uses `jwt.verify()` because it has
|
|
128
|
-
* direct access to `
|
|
159
|
+
* direct access to `ACCESS_TOKEN_SECRET`.
|
|
129
160
|
*
|
|
130
161
|
* **Why session-less user tokens are refused rather than trusted:**
|
|
131
162
|
* every user access token the Oxy API issues carries a `sessionId` (see
|
|
@@ -157,7 +188,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
157
188
|
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
158
189
|
*
|
|
159
190
|
* // Protect all routes under /protected
|
|
160
|
-
* app.use('/protected', oxy.auth({ jwtSecret: process.env.
|
|
191
|
+
* app.use('/protected', oxy.auth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
|
|
161
192
|
*
|
|
162
193
|
* // Access user in route handler
|
|
163
194
|
* app.get('/protected/me', (req, res) => {
|
|
@@ -171,7 +202,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
171
202
|
* app.use('/public', oxy.auth({ optional: true }));
|
|
172
203
|
*
|
|
173
204
|
* // Require a specific scope on a service-token-protected route
|
|
174
|
-
* app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.
|
|
205
|
+
* app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }), oxy.requireScope('files:write'));
|
|
175
206
|
* ```
|
|
176
207
|
*
|
|
177
208
|
* @param options Optional configuration
|
|
@@ -213,7 +244,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
213
244
|
* @example
|
|
214
245
|
* ```typescript
|
|
215
246
|
* // Protect internal endpoints
|
|
216
|
-
* app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.
|
|
247
|
+
* app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
|
|
217
248
|
*
|
|
218
249
|
* app.post('/internal/trigger', (req, res) => {
|
|
219
250
|
* console.log('Service app:', req.serviceApp);
|
|
@@ -242,7 +273,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
242
273
|
* ```typescript
|
|
243
274
|
* app.use(
|
|
244
275
|
* '/internal/files',
|
|
245
|
-
* oxy.serviceAuth({ jwtSecret: process.env.
|
|
276
|
+
* oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }),
|
|
246
277
|
* oxy.requireScope('files:write'),
|
|
247
278
|
* );
|
|
248
279
|
* ```
|
|
@@ -32,6 +32,7 @@ import { OxyServicesChainsMixin } from './OxyServices.chains';
|
|
|
32
32
|
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
33
33
|
import { OxyServicesLinksMixin } from './OxyServices.links';
|
|
34
34
|
import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
|
|
35
|
+
import { OxyServicesInferenceMixin } from './OxyServices.inference';
|
|
35
36
|
import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
36
37
|
import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
37
38
|
/**
|
|
@@ -43,7 +44,7 @@ import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
|
43
44
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
44
45
|
* are visible without a cast.
|
|
45
46
|
*/
|
|
46
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
48
|
/**
|
|
48
49
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
49
50
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -16,6 +16,12 @@ export interface OxyServiceAppContext {
|
|
|
16
16
|
appName: string;
|
|
17
17
|
scopes: string[];
|
|
18
18
|
credentialId: string;
|
|
19
|
+
/**
|
|
20
|
+
* The Oxy account that owns `appId` and is financially responsible for it.
|
|
21
|
+
* Read off the VERIFIED service-token claim set — never a user id, and never
|
|
22
|
+
* the delegated `X-Oxy-User-Id` (ADR 0007).
|
|
23
|
+
*/
|
|
24
|
+
ownerAccountId: string;
|
|
19
25
|
environment: OxyServiceEnvironment;
|
|
20
26
|
}
|
|
21
27
|
export interface OxyServiceActingAsContext {
|
|
@@ -44,6 +50,80 @@ export interface OxyAuthMiddlewareOptions {
|
|
|
44
50
|
}
|
|
45
51
|
export declare function getOxyUserId(req: Request): string | null;
|
|
46
52
|
export declare function isOxyAuthenticated(req: Request): req is OxyAuthenticatedRequest;
|
|
53
|
+
/**
|
|
54
|
+
* The principal a request is CHARGED to, and the identifiers a receipt needs.
|
|
55
|
+
*
|
|
56
|
+
* Every field is read from the verified service-token claim set. It is an
|
|
57
|
+
* OBJECT, not a string, and that is the point: `getOxyUserId` returns a
|
|
58
|
+
* `string | null`, so a delegated end-user id cannot be passed anywhere an
|
|
59
|
+
* `OxyBillingPrincipal` is expected. The confusion ADR 0007 forbids —
|
|
60
|
+
* attributing spend to the person a service is acting for rather than to the
|
|
61
|
+
* service's own account — stops being a code-review question and becomes a
|
|
62
|
+
* compile error.
|
|
63
|
+
*
|
|
64
|
+
* `scopes` are the effective scopes minted into the token (credential ∩
|
|
65
|
+
* application). Nothing re-intersects them here.
|
|
66
|
+
*/
|
|
67
|
+
export interface OxyBillingPrincipal {
|
|
68
|
+
/** `applications.owner_account_id` — the financially responsible account. */
|
|
69
|
+
readonly accountId: string;
|
|
70
|
+
readonly applicationId: string;
|
|
71
|
+
readonly credentialId: string;
|
|
72
|
+
readonly environment: OxyServiceEnvironment;
|
|
73
|
+
readonly scopes: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The full canonical attribution of ADR 0007 for a request: the billing
|
|
77
|
+
* principal PLUS the optional delegated end user.
|
|
78
|
+
*
|
|
79
|
+
* `delegatedUserId` is named for what it is. It answers "on whose behalf" and
|
|
80
|
+
* is absent for a machine credential acting for itself — its absence is normal,
|
|
81
|
+
* and nothing may synthesize one. If removing it would change what any account
|
|
82
|
+
* is charged, the code reading it is wrong.
|
|
83
|
+
*/
|
|
84
|
+
export interface OxyRequestAttribution extends OxyBillingPrincipal {
|
|
85
|
+
readonly delegatedUserId: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The billing principal of a request, or `null` when the request carries no
|
|
89
|
+
* verified service principal (an ordinary user session is not a billable
|
|
90
|
+
* machine principal — its account is resolved from the account graph, not from
|
|
91
|
+
* a token claim).
|
|
92
|
+
*
|
|
93
|
+
* Reads `req.serviceApp` and NOTHING else: not `req.userId`, not `req.user`,
|
|
94
|
+
* not `req.serviceActingAs`. That exclusivity is the invariant this function
|
|
95
|
+
* exists to hold, and `serviceTokenAttribution.test.ts` mutation-tests it.
|
|
96
|
+
*
|
|
97
|
+
* **It answers for the SERVICE-TOKEN lane only.** The API's machine-credential
|
|
98
|
+
* lane (`oxy_sk_*`, issue #972 §2.3) resolves the same five facts into its own
|
|
99
|
+
* `req.machineCredential`, deliberately never `req.serviceApp` — populating the
|
|
100
|
+
* latter would hand a self-serve third-party credential the lane that only
|
|
101
|
+
* platform-trusted applications may enter. So a machine-credential request has
|
|
102
|
+
* no billing principal HERE and resolves `null`, which fails closed: the caller
|
|
103
|
+
* must handle it, and `getRequiredOxyBillingPrincipal` throws rather than
|
|
104
|
+
* charging anyone. One accessor answering for both lanes belongs to the public
|
|
105
|
+
* inference edge that has to admit both, and it needs the machine principal's
|
|
106
|
+
* shape to move into this package first.
|
|
107
|
+
*/
|
|
108
|
+
export declare function getOxyBillingPrincipal(req: Request): OxyBillingPrincipal | null;
|
|
109
|
+
/**
|
|
110
|
+
* {@link getOxyBillingPrincipal}, throwing when the request has none. Use on
|
|
111
|
+
* routes that have already required a service token.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getRequiredOxyBillingPrincipal(req: Request): OxyBillingPrincipal;
|
|
114
|
+
/**
|
|
115
|
+
* The delegated end user of a service request, or `null`.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately reads `req.serviceActingAs` — the grant-verified delegation —
|
|
118
|
+
* and not `req.userId`, which on a non-service request is the caller's own
|
|
119
|
+
* session identity and is not a delegation at all.
|
|
120
|
+
*/
|
|
121
|
+
export declare function getOxyDelegatedUserId(req: Request): string | null;
|
|
122
|
+
/**
|
|
123
|
+
* The whole attribution tuple for a service request: who pays, which
|
|
124
|
+
* application and credential, and optionally on whose behalf.
|
|
125
|
+
*/
|
|
126
|
+
export declare function getOxyRequestAttribution(req: Request): OxyRequestAttribution | null;
|
|
47
127
|
export declare function getRequiredOxyUserId(req: Request): string;
|
|
48
128
|
export declare function requireOxyAuth(req: Request, res: Response, next: NextFunction): void;
|
|
49
129
|
export declare function createOptionalOxyAuth(oxy: OxyServices, options?: OxyAuthMiddlewareOptions): RequestHandler;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* one-label subdomains such as `auth.oxy.so`, `api.oxy.so`,
|
|
16
16
|
* `accounts.oxy.so`, `console.oxy.so`, and `inbox.oxy.so`,
|
|
17
17
|
* - allows the caller's explicit `appOrigins`,
|
|
18
|
+
* - REFUSES the opaque origin on both sides (see `OPAQUE_ORIGIN`),
|
|
18
19
|
* - DENIES everything else (no reflection, never a wildcard with credentials),
|
|
19
20
|
* - echoes back the EXACT matched origin (so credentialed requests work) and
|
|
20
21
|
* sets `Vary: Origin` for correct caching,
|
|
@@ -29,6 +30,11 @@ export interface OxyCorsOptions {
|
|
|
29
30
|
* `https://app.example.com`, `http://localhost:3000`). These are allowed IN
|
|
30
31
|
* ADDITION TO the built-in HTTPS Oxy apex origin family. Each is normalized
|
|
31
32
|
* via `new URL().origin`.
|
|
33
|
+
*
|
|
34
|
+
* An entry that is not a URL, or whose origin is the opaque origin
|
|
35
|
+
* (`exp://…`, `capacitor://…`, `chrome-extension://…`, `file:`, `data:`), is
|
|
36
|
+
* DROPPED with an error log rather than admitted — see `OPAQUE_ORIGIN` for
|
|
37
|
+
* why one such entry would otherwise admit every other one.
|
|
32
38
|
*/
|
|
33
39
|
appOrigins?: string[];
|
|
34
40
|
/**
|
|
@@ -46,6 +52,41 @@ export interface OxyCorsOptions {
|
|
|
46
52
|
/** Preflight cache lifetime in seconds. Default 86400 (24h). */
|
|
47
53
|
maxAgeSeconds?: number;
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Normalize the configured `appOrigins` into the exact-match set — the
|
|
57
|
+
* CONFIGURE-SIDE half of the opaque-origin guard.
|
|
58
|
+
*
|
|
59
|
+
* An entry that is not a URL, or whose origin is opaque, is dropped and named
|
|
60
|
+
* in an error log. Dropped rather than thrown on because `appOrigins` is
|
|
61
|
+
* deployment configuration — at least one Oxy backend reads it from the
|
|
62
|
+
* environment — and a typo there must cost that one origin its CORS headers,
|
|
63
|
+
* never the whole service its boot. Both failure modes are equally SAFE (the
|
|
64
|
+
* entry is absent from the set either way), so the choice is purely about
|
|
65
|
+
* blast radius, and dropping keeps it to one origin whose requests then fail
|
|
66
|
+
* visibly in the browser.
|
|
67
|
+
*
|
|
68
|
+
* Exported for `__tests__/cors.socket.test.ts` and NOT re-exported from
|
|
69
|
+
* `server/index.ts`, so it is not part of the package's public surface. The
|
|
70
|
+
* two halves of the guard are separately exported because they are separately
|
|
71
|
+
* testable only that way: with this half in place the match-side half is
|
|
72
|
+
* unreachable through `createOxyCors`, so a test driving the public API alone
|
|
73
|
+
* would measure this function twice and the other one never.
|
|
74
|
+
*/
|
|
75
|
+
export declare function normalizeAppOrigins(appOrigins: string[]): Set<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Whether `origin` may be echoed back: it is in the built-in HTTPS Oxy apex
|
|
78
|
+
* family, or it exactly matches one of the configured app origins.
|
|
79
|
+
*
|
|
80
|
+
* The opaque-origin refusal here is the MATCH-SIDE half of the guard, and it
|
|
81
|
+
* is what makes the property hold regardless of how `explicit` was built — a
|
|
82
|
+
* set that somehow contains `"null"` still matches nothing, because no
|
|
83
|
+
* incoming origin ever normalizes past this line. `normalizeAppOrigins` is
|
|
84
|
+
* what stops such a set existing today; this is what stops it mattering.
|
|
85
|
+
*
|
|
86
|
+
* Exported for the same reason as `normalizeAppOrigins`, and likewise absent
|
|
87
|
+
* from `server/index.ts`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function matchesAllowedOrigin(explicit: ReadonlySet<string>, origin: string): boolean;
|
|
49
90
|
/**
|
|
50
91
|
* Create a strict Oxy CORS middleware. See module docs.
|
|
51
92
|
*
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* app.use(createOxyRateLimit(oxy, { store: redisStore }));
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyUserId, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth';
|
|
18
|
-
export type { OxyAuthenticatedRequest, OxyAuthMiddlewareOptions, OxyAuthRequest, OxyRequestUser, OxyServiceActingAsContext, OxyServiceAppContext, OxyServiceEnvironment, } from './auth';
|
|
17
|
+
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth';
|
|
18
|
+
export type { OxyAuthenticatedRequest, OxyAuthMiddlewareOptions, OxyAuthRequest, OxyBillingPrincipal, OxyRequestAttribution, OxyRequestUser, OxyServiceActingAsContext, OxyServiceAppContext, OxyServiceEnvironment, } from './auth';
|
|
19
19
|
export { createOxyRateLimit } from './rateLimit';
|
|
20
20
|
export type { OxyRateLimitOptions } from './rateLimit';
|
|
21
21
|
export { assertSafePublicUrl, isBlockedIp, safeFetch, SsrfRejection, UpstreamError, ALLOWED_PORTS, ALLOWED_PROTOCOLS, BLOCKED_HOSTNAMES, DEFAULT_USER_AGENT, MAX_REDIRECTS, MAX_URL_LENGTH, UPSTREAM_HEADERS_TIMEOUT_MS, } from './safeFetch';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "21.0.
|
|
3
|
+
"version": "21.0.2",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"dependencies": {
|
|
117
117
|
"@noble/ciphers": "^1.3.0",
|
|
118
118
|
"@noble/hashes": "^1.8.0",
|
|
119
|
-
"@oxyhq/contracts": "^0.
|
|
119
|
+
"@oxyhq/contracts": "^0.30.0",
|
|
120
120
|
"@oxyhq/protocol": "^0.2.0",
|
|
121
121
|
"@scure/bip39": "^1.6.0",
|
|
122
122
|
"@types/elliptic": "^6.4.18",
|
package/src/HttpService.ts
CHANGED
|
@@ -913,6 +913,19 @@ export class HttpService {
|
|
|
913
913
|
*/
|
|
914
914
|
private static readonly CACHE_IDENTITY_DELIM = ' id=';
|
|
915
915
|
|
|
916
|
+
/**
|
|
917
|
+
* The keys whose presence beside `data` makes a body a PAGE rather than a
|
|
918
|
+
* payload — see {@link unwrapResponse} for why this list is narrow.
|
|
919
|
+
*
|
|
920
|
+
* - `pagination` — the offset-paginated house envelope (`sendPaginated`).
|
|
921
|
+
* - `nextCursor` — the keyset-paginated one (the account audit trails).
|
|
922
|
+
*
|
|
923
|
+
* Membership is decided by key PRESENCE, never by value: the last page sends
|
|
924
|
+
* `nextCursor: null`, and an envelope that collapsed into a bare payload
|
|
925
|
+
* exactly when the stream ended would be a worse bug than the one this fixes.
|
|
926
|
+
*/
|
|
927
|
+
private static readonly PAGE_ENVELOPE_KEYS: readonly string[] = ['pagination', 'nextCursor'];
|
|
928
|
+
|
|
916
929
|
/**
|
|
917
930
|
* Derive a stable, non-sensitive identity discriminator for cache scoping.
|
|
918
931
|
*
|
|
@@ -1212,21 +1225,48 @@ export class HttpService {
|
|
|
1212
1225
|
}
|
|
1213
1226
|
|
|
1214
1227
|
/**
|
|
1215
|
-
* Unwrap standardized API response
|
|
1228
|
+
* Unwrap the standardized API response envelope — EXCEPT when the envelope is
|
|
1229
|
+
* a page, in which case it travels whole.
|
|
1230
|
+
*
|
|
1231
|
+
* `{ data: <payload> }` is the house success envelope (`sendSuccess`), and
|
|
1232
|
+
* reducing it to `<payload>` is what every call site in the SDK expects. But
|
|
1233
|
+
* the reduction DISCARDS every sibling key, silently, and a page's siblings
|
|
1234
|
+
* are the only thing that says where the next page starts. That is how
|
|
1235
|
+
* `GET /accounts/:id/audit` lost its `nextCursor`: the caller received a bare
|
|
1236
|
+
* array, `getNextPageParam` read `undefined`, and pagination was dead past the
|
|
1237
|
+
* first page with nothing to show that it was.
|
|
1238
|
+
*
|
|
1239
|
+
* ## Why the rule is narrow, and not "any sibling key survives"
|
|
1240
|
+
*
|
|
1241
|
+
* "An object carrying `data` plus anything else is not an envelope" is the
|
|
1242
|
+
* tempting general rule, and it is wrong here: this API already answers
|
|
1243
|
+
* `{ data, count }` on ~15 routes, plus `{ data, source }`, `{ data, reason }`
|
|
1244
|
+
* and `{ data, secretDestroyed }`, and a dozen measured Console call sites
|
|
1245
|
+
* type those as the bare payload (`Array<ProviderConnection>`,
|
|
1246
|
+
* `AccountBillingState | null`, …). Preserving those envelopes would hand every
|
|
1247
|
+
* one of them an object where it expects its payload — at runtime only, since
|
|
1248
|
+
* the response type is a call-site assertion. So the rule names PAGINATION
|
|
1249
|
+
* specifically: `data` beside {@link PAGE_ENVELOPE_KEYS} is a page.
|
|
1250
|
+
*
|
|
1251
|
+
* A route whose sibling key genuinely matters to its caller belongs in that
|
|
1252
|
+
* list, or should not be a sibling of `data` at all — the cursor-paginated
|
|
1253
|
+
* surfaces already in the SDK (`{ follows, nextCursor }`,
|
|
1254
|
+
* `{ records, nextCursor }`) sidestep this by never using `data`.
|
|
1216
1255
|
*/
|
|
1217
1256
|
private unwrapResponse(responseData: unknown): unknown {
|
|
1218
|
-
|
|
1219
|
-
|
|
1257
|
+
if (!responseData || typeof responseData !== 'object' || !('data' in responseData)) {
|
|
1258
|
+
// Not the success envelope (or not an object at all) — as-is.
|
|
1220
1259
|
return responseData;
|
|
1221
1260
|
}
|
|
1222
|
-
|
|
1223
|
-
//
|
|
1224
|
-
|
|
1225
|
-
|
|
1261
|
+
|
|
1262
|
+
// A page travels whole: its cursor/pagination sibling is unrecoverable
|
|
1263
|
+
// information, not decoration.
|
|
1264
|
+
if (HttpService.PAGE_ENVELOPE_KEYS.some((key) => key in responseData)) {
|
|
1265
|
+
return responseData;
|
|
1226
1266
|
}
|
|
1227
|
-
|
|
1228
|
-
//
|
|
1229
|
-
return responseData;
|
|
1267
|
+
|
|
1268
|
+
// Regular success envelope: `{ data: ... }` -> the payload.
|
|
1269
|
+
return Array.isArray(responseData) ? responseData : responseData.data;
|
|
1230
1270
|
}
|
|
1231
1271
|
|
|
1232
1272
|
/**
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HttpService.unwrapResponse` envelope tests — driven through the REAL class
|
|
3
|
+
* against a stubbed `fetch`, never by calling the private method.
|
|
4
|
+
*
|
|
5
|
+
* The convenience unwrap reduces the house `{ data: <payload> }` envelope to its
|
|
6
|
+
* payload, and in doing so it discards every sibling key. That silently killed
|
|
7
|
+
* pagination on the account audit trails (`GET /accounts/:id/audit` and
|
|
8
|
+
* `GET /accounts/:id/billing/audit`, which answer `{ data, count, nextCursor }`):
|
|
9
|
+
* the caller got a bare array, `getNextPageParam` read `undefined`, and there was
|
|
10
|
+
* nothing at the call site to show that page 2 could never be requested.
|
|
11
|
+
*
|
|
12
|
+
* The three cases below are the whole contract, and each is load-bearing:
|
|
13
|
+
*
|
|
14
|
+
* - a cursor page travels WHOLE (the regression),
|
|
15
|
+
* - a `{ data, pagination }` page still travels whole (the behaviour that
|
|
16
|
+
* already existed — the positive control for "pages are preserved"),
|
|
17
|
+
* - a bare `{ data }` body still unwraps (the control proving the convenience
|
|
18
|
+
* every other call site depends on is intact).
|
|
19
|
+
*
|
|
20
|
+
* Reverting the fix must redden the first and leave the other two green.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { HttpService } from '../HttpService';
|
|
24
|
+
|
|
25
|
+
/** One entry of an audit page — only the shape matters here. */
|
|
26
|
+
const ENTRY = { source: 'application_credential', eventType: 'created' } as const;
|
|
27
|
+
|
|
28
|
+
function jsonBody(body: unknown): Response {
|
|
29
|
+
return new Response(JSON.stringify(body), {
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: { 'content-type': 'application/json' },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('HttpService response-envelope unwrapping', () => {
|
|
36
|
+
const originalFetch = globalThis.fetch;
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
globalThis.fetch = originalFetch;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Drive a real `HttpService.get` against a stubbed wire body. */
|
|
43
|
+
async function fetchWireBody<T>(body: unknown): Promise<T> {
|
|
44
|
+
globalThis.fetch = jest.fn(async () => jsonBody(body)) as unknown as typeof fetch;
|
|
45
|
+
const http = new HttpService({ baseURL: 'http://api.test.invalid' });
|
|
46
|
+
return http.get<T>('/accounts/acct-1/audit', { cache: false });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('preserves a cursor page whole — `count` and `nextCursor` reach the caller', async () => {
|
|
50
|
+
const wire = { data: [ENTRY], count: 1, nextCursor: 'CURSOR-XYZ' };
|
|
51
|
+
|
|
52
|
+
const page = await fetchWireBody<typeof wire>(wire);
|
|
53
|
+
|
|
54
|
+
expect(page).toEqual(wire);
|
|
55
|
+
// The cursor is the only thing that says where page 2 starts. Asserted on
|
|
56
|
+
// its own because `toEqual` above would also pass a body that merely
|
|
57
|
+
// happened to be an array of one entry.
|
|
58
|
+
expect(page.nextCursor).toBe('CURSOR-XYZ');
|
|
59
|
+
expect(page.count).toBe(1);
|
|
60
|
+
expect(page.data).toEqual([ENTRY]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves the LAST cursor page, where `nextCursor` is null', async () => {
|
|
64
|
+
// The recognition is by key presence, not truthiness: if the envelope
|
|
65
|
+
// collapsed into a bare array exactly when the stream ended, every caller
|
|
66
|
+
// would crash on the final page instead of finishing.
|
|
67
|
+
const wire = { data: [ENTRY], count: 1, nextCursor: null };
|
|
68
|
+
|
|
69
|
+
const page = await fetchWireBody<typeof wire>(wire);
|
|
70
|
+
|
|
71
|
+
expect(page).toEqual(wire);
|
|
72
|
+
expect(page.nextCursor).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('preserves the offset-paginated `{ data, pagination }` envelope (control)', async () => {
|
|
76
|
+
const wire = {
|
|
77
|
+
data: [ENTRY],
|
|
78
|
+
pagination: { total: 1, limit: 50, offset: 0, hasMore: false },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const page = await fetchWireBody<typeof wire>(wire);
|
|
82
|
+
|
|
83
|
+
expect(page).toEqual(wire);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('still unwraps a bare `{ data }` success envelope to its payload (control)', async () => {
|
|
87
|
+
const payload = { id: 'acct-1', name: 'Acme' };
|
|
88
|
+
|
|
89
|
+
const unwrapped = await fetchWireBody<typeof payload>({ data: payload });
|
|
90
|
+
|
|
91
|
+
expect(unwrapped).toEqual(payload);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('still unwraps `{ data, count }`, which a dozen callers type as the bare payload', async () => {
|
|
95
|
+
// `{ data, count }` is answered by ~15 routes whose Console call sites type
|
|
96
|
+
// the result `Array<T>`. `count` is `data.length` — recoverable — so this
|
|
97
|
+
// envelope deliberately does NOT survive, and widening the rule to "any
|
|
98
|
+
// sibling key" would break every one of those callers at runtime.
|
|
99
|
+
const entries = [ENTRY, ENTRY];
|
|
100
|
+
|
|
101
|
+
const unwrapped = await fetchWireBody<typeof entries>({ data: entries, count: 2 });
|
|
102
|
+
|
|
103
|
+
expect(unwrapped).toEqual(entries);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('passes through a body that has no `data` key at all', async () => {
|
|
107
|
+
// The cursor surfaces already in the SDK (`{ follows, nextCursor }`,
|
|
108
|
+
// `{ records, nextCursor }`) rely on this lane.
|
|
109
|
+
const wire = { follows: [{ relationshipId: 'rel-1' }], nextCursor: 'CURSOR-ABC' };
|
|
110
|
+
|
|
111
|
+
const passed = await fetchWireBody<typeof wire>(wire);
|
|
112
|
+
|
|
113
|
+
expect(passed).toEqual(wire);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -2136,8 +2136,13 @@
|
|
|
2136
2136
|
"filesWrite": "Upload and modify your files",
|
|
2137
2137
|
"filesDelete": "Delete your files",
|
|
2138
2138
|
"webhooksReceive": "Receive webhooks",
|
|
2139
|
-
"
|
|
2140
|
-
"
|
|
2139
|
+
"inferenceInvoke": "Run AI requests on your behalf",
|
|
2140
|
+
"inferenceModelsRead": "List available AI models",
|
|
2141
|
+
"inferenceUsageRead": "Read its AI usage and costs",
|
|
2142
|
+
"inferenceRoutingRead": "Read how AI requests are routed",
|
|
2143
|
+
"inferenceRoutingWrite": "Change how AI requests are routed",
|
|
2144
|
+
"inferenceProvidersRead": "Read its connected AI providers",
|
|
2145
|
+
"inferenceProvidersWrite": "Manage its connected AI providers",
|
|
2141
2146
|
"federationWrite": "Act across federated services"
|
|
2142
2147
|
},
|
|
2143
2148
|
"account": {
|
|
@@ -2136,8 +2136,13 @@
|
|
|
2136
2136
|
"filesWrite": "Subir y modificar tus archivos",
|
|
2137
2137
|
"filesDelete": "Eliminar tus archivos",
|
|
2138
2138
|
"webhooksReceive": "Recibir webhooks",
|
|
2139
|
-
"
|
|
2140
|
-
"
|
|
2139
|
+
"inferenceInvoke": "Ejecutar peticiones de IA en tu nombre",
|
|
2140
|
+
"inferenceModelsRead": "Ver los modelos de IA disponibles",
|
|
2141
|
+
"inferenceUsageRead": "Ver su consumo y costes de IA",
|
|
2142
|
+
"inferenceRoutingRead": "Ver cómo se enrutan las peticiones de IA",
|
|
2143
|
+
"inferenceRoutingWrite": "Cambiar cómo se enrutan las peticiones de IA",
|
|
2144
|
+
"inferenceProvidersRead": "Ver sus proveedores de IA conectados",
|
|
2145
|
+
"inferenceProvidersWrite": "Gestionar sus proveedores de IA conectados",
|
|
2141
2146
|
"federationWrite": "Actuar en servicios federados"
|
|
2142
2147
|
},
|
|
2143
2148
|
"account": {
|