@oxy.so/core 4.0.0 → 4.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.
- package/dist/cjs/HttpService.js +142 -33
- package/dist/cjs/api/accounts.js +49 -10
- package/dist/cjs/api/apps.js +2 -2
- package/dist/cjs/api/auth.js +24 -3
- package/dist/cjs/api/billing.js +35 -0
- package/dist/cjs/boot/sessionColdBoot.js +28 -57
- package/dist/cjs/inference/OxyInferenceClient.js +53 -20
- package/dist/cjs/server/OxyServer.js +107 -51
- package/dist/cjs/server/agentAccount.js +19 -0
- package/dist/cjs/server/auth.js +19 -0
- package/dist/cjs/server/capabilityTicket.js +3 -41
- package/dist/cjs/server/index.js +10 -2
- package/dist/cjs/server/liveCapabilityTicket.js +48 -0
- package/dist/cjs/server/middleware.js +60 -18
- package/dist/cjs/server/namespaces.js +57 -0
- package/dist/cjs/server/serviceTokenLifetime.js +13 -0
- package/dist/cjs/session/SessionClient.js +90 -12
- package/dist/cjs/session/authStateStore.js +130 -39
- package/dist/cjs/session/refresh.js +152 -30
- package/dist/cjs/session/sharedDeviceCredential.js +17 -1
- package/dist/cjs/utils/officialOrigins.js +7 -18
- package/dist/esm/HttpService.js +143 -34
- package/dist/esm/api/accounts.js +38 -0
- package/dist/esm/api/apps.js +2 -2
- package/dist/esm/api/auth.js +23 -3
- package/dist/esm/api/billing.js +35 -0
- package/dist/esm/boot/sessionColdBoot.js +29 -58
- package/dist/esm/inference/OxyInferenceClient.js +46 -13
- package/dist/esm/server/OxyServer.js +107 -51
- package/dist/esm/server/agentAccount.js +16 -0
- package/dist/esm/server/auth.js +18 -0
- package/dist/esm/server/capabilityTicket.js +1 -39
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/liveCapabilityTicket.js +45 -0
- package/dist/esm/server/middleware.js +60 -18
- package/dist/esm/server/namespaces.js +57 -0
- package/dist/esm/server/serviceTokenLifetime.js +9 -0
- package/dist/esm/session/SessionClient.js +90 -12
- package/dist/esm/session/authStateStore.js +128 -38
- package/dist/esm/session/refresh.js +151 -30
- package/dist/esm/session/sharedDeviceCredential.js +17 -1
- package/dist/esm/utils/officialOrigins.js +2 -13
- package/dist/types/HttpService.d.ts +26 -12
- package/dist/types/api/accounts.d.ts +38 -0
- package/dist/types/api/agency.d.ts +3 -1
- package/dist/types/api/apps.d.ts +3 -1
- package/dist/types/api/auth.d.ts +12 -1
- package/dist/types/api/billing.d.ts +12 -0
- package/dist/types/api/session.d.ts +3 -1
- package/dist/types/boot/sessionColdBoot.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +12 -0
- package/dist/types/inference/index.d.ts +1 -1
- package/dist/types/server/OxyServer.d.ts +16 -9
- package/dist/types/server/agentAccount.d.ts +14 -0
- package/dist/types/server/auth.d.ts +18 -0
- package/dist/types/server/capabilityTicket.d.ts +2 -4
- package/dist/types/server/index.d.ts +7 -1
- package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
- package/dist/types/server/middleware.d.ts +15 -6
- package/dist/types/server/namespaces.d.ts +78 -2
- package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
- package/dist/types/session/SessionClient.d.ts +8 -0
- package/dist/types/session/authStateStore.d.ts +15 -0
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +32 -7
- package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
- package/dist/types/utils/officialOrigins.d.ts +1 -5
- package/package.json +6 -6
- package/src/HttpService.ts +138 -50
- package/src/api/accounts.ts +22 -0
- package/src/api/agency.ts +3 -1
- package/src/api/apps.ts +2 -2
- package/src/api/auth.ts +31 -2
- package/src/api/billing.ts +71 -9
- package/src/api/session.ts +3 -1
- package/src/boot/sessionColdBoot.ts +27 -72
- package/src/index.ts +1 -1
- package/src/inference/OxyInferenceClient.ts +50 -5
- package/src/inference/index.ts +1 -0
- package/src/server/OxyServer.ts +114 -53
- package/src/server/agentAccount.ts +28 -0
- package/src/server/auth.ts +25 -0
- package/src/server/capabilityTicket.ts +2 -42
- package/src/server/index.ts +13 -0
- package/src/server/liveCapabilityTicket.ts +47 -0
- package/src/server/middleware.ts +72 -25
- package/src/server/namespaces.ts +99 -2
- package/src/server/serviceTokenLifetime.ts +13 -0
- package/src/session/SessionClient.ts +85 -12
- package/src/session/authStateStore.ts +130 -46
- package/src/session/index.ts +1 -0
- package/src/session/refresh.ts +144 -28
- package/src/session/sharedDeviceCredential.ts +14 -2
- package/src/utils/officialOrigins.ts +2 -13
package/src/api/billing.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
10
10
|
* arrive as ISO-8601 strings.
|
|
11
11
|
*/
|
|
12
|
+
import { type SubjectProductAccess,
|
|
13
|
+
type SubjectProductAccessQuery,
|
|
14
|
+
subjectProductAccessQuerySchema, subjectProductAccessSchema, } from '@oxy.so/contracts';
|
|
15
|
+
import {
|
|
16
|
+
type ProductSubscriptionSummary, type SubscriptionCreditGrant, type ProductSubscriptionCancellationResult,
|
|
17
|
+
productSubscriptionCancellationResultSchema,
|
|
18
|
+
cancelProductSubscriptionSchema,
|
|
19
|
+
productSubscriptionsResponseSchema,
|
|
20
|
+
subscriptionCreditGrantsResponseSchema,
|
|
21
|
+
} from '@oxy.so/contracts';
|
|
12
22
|
import type { OxyContext } from '../client/context';
|
|
13
23
|
|
|
14
24
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
@@ -105,13 +115,65 @@ export interface WalletTransactionsPage {
|
|
|
105
115
|
export class BillingApi {
|
|
106
116
|
constructor(protected readonly ctx: OxyContext) {}
|
|
107
117
|
|
|
108
|
-
/**
|
|
109
|
-
async
|
|
118
|
+
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
119
|
+
async productAccess(query: SubjectProductAccessQuery): Promise<SubjectProductAccess> {
|
|
120
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
121
|
+
const response = await this.ctx.request<unknown>('GET',
|
|
122
|
+
`/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}`,
|
|
123
|
+
undefined, { cache: false });
|
|
124
|
+
return subjectProductAccessSchema.parse(response);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
128
|
+
async productSubscriptions(): Promise<ProductSubscriptionSummary[]> {
|
|
129
|
+
return productSubscriptionsResponseSchema.parse(
|
|
130
|
+
await this.ctx.request<unknown>(
|
|
131
|
+
"GET",
|
|
132
|
+
"/billing/product-subscriptions",
|
|
133
|
+
undefined,
|
|
134
|
+
{ cache: false },
|
|
135
|
+
),
|
|
136
|
+
).subscriptions;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
140
|
+
async creditGrants(): Promise<SubscriptionCreditGrant[]> {
|
|
141
|
+
return subscriptionCreditGrantsResponseSchema.parse(
|
|
142
|
+
await this.ctx.request<unknown>(
|
|
143
|
+
"GET",
|
|
144
|
+
"/billing/credit-grants",
|
|
145
|
+
undefined,
|
|
146
|
+
{ cache: false },
|
|
147
|
+
),
|
|
148
|
+
).grants;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
152
|
+
async cancelProductSubscription(sourceId: string, expectedSubjectAccountId?: string): Promise<void> {
|
|
153
|
+
await this.ctx.request(
|
|
154
|
+
"POST",
|
|
155
|
+
"/billing/product-subscriptions/cancel",
|
|
156
|
+
cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
157
|
+
{ cache: false },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
162
|
+
async cancelProductSubscriptionWithStatus(sourceId: string, expectedSubjectAccountId?: string): Promise<ProductSubscriptionCancellationResult> {
|
|
163
|
+
return productSubscriptionCancellationResultSchema.parse(await this.ctx.request<unknown>(
|
|
164
|
+
'POST', '/billing/product-subscriptions/cancel',
|
|
165
|
+
cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
166
|
+
{ cache: false },
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The signed-in user's payment history. Never cached. */
|
|
171
|
+
async payments(): Promise<Payment[]> {
|
|
110
172
|
return this.ctx.request<Payment[]>('GET', '/payments/user', undefined, { cache: false });
|
|
111
173
|
}
|
|
112
174
|
|
|
113
|
-
|
|
114
|
-
|
|
175
|
+
/** A user's subscription (default: the signed-in user's). */
|
|
176
|
+
async subscription(userId?: string): Promise<Subscription> {
|
|
115
177
|
const id = this.resolveUserId(userId);
|
|
116
178
|
return this.ctx.request<Subscription>('GET', `/subscription/${id}`, undefined, {
|
|
117
179
|
cache: true,
|
|
@@ -119,14 +181,14 @@ export class BillingApi {
|
|
|
119
181
|
});
|
|
120
182
|
}
|
|
121
183
|
|
|
122
|
-
|
|
123
|
-
|
|
184
|
+
/** A user's wallet (default: the signed-in user's). Cached briefly: the balance moves. */
|
|
185
|
+
async wallet(userId?: string): Promise<Wallet> {
|
|
124
186
|
const id = this.resolveUserId(userId);
|
|
125
187
|
return this.ctx.request<Wallet>('GET', `/wallet/${id}`, undefined, { cache: true, cacheTTL: WALLET_TTL });
|
|
126
188
|
}
|
|
127
189
|
|
|
128
|
-
|
|
129
|
-
|
|
190
|
+
/** A page of a user's wallet transactions (default: the signed-in user's). Never cached. */
|
|
191
|
+
async walletTransactions(
|
|
130
192
|
options: { userId?: string; limit?: number; offset?: number } = {},
|
|
131
193
|
): Promise<WalletTransactionsPage> {
|
|
132
194
|
const id = this.resolveUserId(options.userId);
|
|
@@ -136,7 +198,7 @@ export class BillingApi {
|
|
|
136
198
|
return this.ctx.request<WalletTransactionsPage>('GET', `/wallet/transactions/${id}`, params, { cache: false });
|
|
137
199
|
}
|
|
138
200
|
|
|
139
|
-
|
|
201
|
+
private resolveUserId(userId?: string): string {
|
|
140
202
|
const id = userId || this.ctx.oxy.session.userId;
|
|
141
203
|
if (!id) throw new Error('User not authenticated');
|
|
142
204
|
return id;
|
package/src/api/session.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* token belongs to: validating it, listing the sessions beside it, signing out.
|
|
7
7
|
*/
|
|
8
8
|
import { jwtDecode } from 'jwt-decode';
|
|
9
|
-
import type { DeviceProof } from '@oxy.so/contracts';
|
|
9
|
+
import type { AccountActorChain, DeviceProof } from '@oxy.so/contracts';
|
|
10
10
|
import type { OxyContext } from '../client/context';
|
|
11
11
|
import type { ClientSession } from '../models/session';
|
|
12
12
|
import type { User } from '../models/interfaces';
|
|
@@ -26,6 +26,8 @@ export interface SessionValidation {
|
|
|
26
26
|
user: User;
|
|
27
27
|
sessionId?: string;
|
|
28
28
|
source?: string;
|
|
29
|
+
/** Who acted, and as whom, read off the session row (issue #1520). */
|
|
30
|
+
actor?: AccountActorChain;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
interface AccessTokenClaims {
|
|
@@ -44,17 +44,14 @@ import { runColdBoot, type ColdBootOutcome, type ColdBootStep } from '../utils/c
|
|
|
44
44
|
import { isNative as detectNative } from '../utils/platform';
|
|
45
45
|
import { logger } from '../logger';
|
|
46
46
|
import { computeIdentityTag } from '../utils/cacheKey';
|
|
47
|
-
import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm } from '../session/refresh';
|
|
47
|
+
import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm, refreshSharedDeviceArm } from '../session/refresh';
|
|
48
48
|
import {
|
|
49
49
|
establishIdentitySession,
|
|
50
50
|
resolveIdentityPin,
|
|
51
51
|
type IdentityBinding,
|
|
52
52
|
} from '../session/identitySession';
|
|
53
53
|
import type { IdentityPin } from '../session/identityPin';
|
|
54
|
-
import {
|
|
55
|
-
decideSharedDeviceJoin,
|
|
56
|
-
type SharedDeviceCredentialStore,
|
|
57
|
-
} from '../session/sharedDeviceCredential';
|
|
54
|
+
import type { SharedDeviceCredentialStore } from '../session/sharedDeviceCredential';
|
|
58
55
|
import type { OxyServices } from '../OxyServices';
|
|
59
56
|
import type { AuthStateStore, PersistedAuthState } from '../session/authStateStore';
|
|
60
57
|
import type { DeviceSessionState } from '@oxy.so/contracts';
|
|
@@ -178,6 +175,8 @@ export async function runSessionColdBoot(
|
|
|
178
175
|
// Boot-local (not module-level) so it cannot leak across boots or break under
|
|
179
176
|
// bundler re-evaluation.
|
|
180
177
|
let signedOutReason: SignedOutReason = 'no_session';
|
|
178
|
+
let rejectedLocalHolder = false;
|
|
179
|
+
let accountRecoverySuperseded = false;
|
|
181
180
|
|
|
182
181
|
// Boot-local memo for the identity pin. Resolved lazily INSIDE a step so the
|
|
183
182
|
// (local, but storage-backed) read is covered by `overallDeadlineMs`, and
|
|
@@ -212,6 +211,7 @@ export async function runSessionColdBoot(
|
|
|
212
211
|
steps.push({
|
|
213
212
|
id: 'warm-token-plant',
|
|
214
213
|
run: async () => {
|
|
214
|
+
if (identityBinding === null && isNative && await store.isAutomaticIdentitySignInSuppressed?.()) return { kind: 'skip' };
|
|
215
215
|
const persisted = await store.load();
|
|
216
216
|
if (!persisted?.accessToken || !persisted.sessionId || !persisted.userId || !persisted.expiresAt) {
|
|
217
217
|
return { kind: 'skip' };
|
|
@@ -287,6 +287,11 @@ export async function runSessionColdBoot(
|
|
|
287
287
|
},
|
|
288
288
|
};
|
|
289
289
|
case 'invalid-secret': {
|
|
290
|
+
if (identityBinding === null && store.setAutomaticIdentitySignInSuppressed && await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
291
|
+
rejectedLocalHolder = true;
|
|
292
|
+
signedOutReason = 'no_session';
|
|
293
|
+
return {kind: 'skip'};
|
|
294
|
+
}
|
|
290
295
|
// Stale/diverged secret — drop it so the mint lane stops firing. On
|
|
291
296
|
// native the Commons-proof step below can still recover; on web this ends
|
|
292
297
|
// signed out. Setting it undefined drops the key on the store's JSON
|
|
@@ -298,6 +303,7 @@ export async function runSessionColdBoot(
|
|
|
298
303
|
return { kind: 'skip' };
|
|
299
304
|
}
|
|
300
305
|
case 'no-session':
|
|
306
|
+
rejectedLocalHolder = identityBinding === null;
|
|
301
307
|
// Device known, no live session — authoritative signed-out. Keep the
|
|
302
308
|
// secret (the device may sign in again).
|
|
303
309
|
signedOutReason = 'no_session';
|
|
@@ -329,7 +335,9 @@ export async function runSessionColdBoot(
|
|
|
329
335
|
);
|
|
330
336
|
return { kind: 'skip' };
|
|
331
337
|
case 'session-ended':
|
|
332
|
-
//
|
|
338
|
+
// The prior holder/epoch was superseded. This boot must not turn a
|
|
339
|
+
// late rejection into a fresh Commons sign-in; a new boot can reconcile.
|
|
340
|
+
if (identityBinding === null) accountRecoverySuperseded = true;
|
|
333
341
|
return { kind: 'skip' };
|
|
334
342
|
case 'no-secret':
|
|
335
343
|
return { kind: 'skip' };
|
|
@@ -394,73 +402,16 @@ export async function runSessionColdBoot(
|
|
|
394
402
|
// The adoption itself is local, but it is only worth committing alongside
|
|
395
403
|
// a mint that proves the credential — so the whole lane is online-gated
|
|
396
404
|
// like every other network step.
|
|
397
|
-
enabled: () => isNative && !isOffline(),
|
|
405
|
+
enabled: () => isNative && !isOffline() && !accountRecoverySuperseded,
|
|
398
406
|
run: async () => {
|
|
399
|
-
const
|
|
400
|
-
const decision = decideSharedDeviceJoin(before, await sharedSlot.read());
|
|
401
|
-
if (decision.action === 'skip') {
|
|
402
|
-
logger.debug(
|
|
403
|
-
`shared device credential not adopted (${decision.reason})`,
|
|
404
|
-
{ component: 'sessionColdBoot', method: 'shared-device-adopt' },
|
|
405
|
-
);
|
|
406
|
-
return { kind: 'skip' };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Restore the store to exactly what it held before this lane touched it.
|
|
410
|
-
// A credential we adopted and could not prove must not be left behind for
|
|
411
|
-
// the next boot's mint lane to keep retrying.
|
|
412
|
-
const revert = async (): Promise<void> => {
|
|
413
|
-
if (before) {
|
|
414
|
-
await store.save(before);
|
|
415
|
-
} else {
|
|
416
|
-
await store.clear();
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const adopted: PersistedAuthState = {
|
|
421
|
-
// The mint fills both in from the device's live state; carrying the
|
|
422
|
-
// previous session's ids into a different device session would be a
|
|
423
|
-
// lie for however long the mint takes.
|
|
424
|
-
sessionId: '',
|
|
425
|
-
userId: '',
|
|
426
|
-
deviceId: decision.credential.deviceId,
|
|
427
|
-
deviceSecret: decision.credential.deviceSecret,
|
|
428
|
-
};
|
|
429
|
-
if (!(await store.save(adopted))) {
|
|
430
|
-
logger.error(
|
|
431
|
-
'adopted the shared device credential but it could not be durably persisted — reverting',
|
|
432
|
-
undefined,
|
|
433
|
-
{ component: 'sessionColdBoot', method: 'shared-device-adopt' },
|
|
434
|
-
);
|
|
435
|
-
await revert();
|
|
436
|
-
return { kind: 'skip' };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const result = await refreshDeviceSecretArm({ oxy, store, pin: null });
|
|
407
|
+
const result = await refreshSharedDeviceArm({oxy, store, shared: sharedSlot, rejectedLocalHolder});
|
|
440
408
|
if (result.status === 'ok') {
|
|
441
|
-
return {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
sessionId: result.sessionId,
|
|
445
|
-
userId: result.userId,
|
|
446
|
-
accessToken: result.token,
|
|
447
|
-
},
|
|
448
|
-
};
|
|
409
|
+
return {kind: 'session', session: {
|
|
410
|
+
sessionId: result.sessionId, userId: result.userId, accessToken: result.token, state: result.state,
|
|
411
|
+
}};
|
|
449
412
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// The one place we hold POSITIVE proof that the exact bytes in the
|
|
453
|
-
// shared slot are dead — the server rejected them by name. Clearing it
|
|
454
|
-
// signs nobody out (a credential the server does not recognise cannot
|
|
455
|
-
// be minting for anyone) and it is what stops a dead credential from
|
|
456
|
-
// blocking every future install: a stale slot owned by a different
|
|
457
|
-
// `deviceId` is otherwise never overwritten, by design.
|
|
458
|
-
await sharedSlot.clear();
|
|
459
|
-
} else if (result.status === 'no-session') {
|
|
460
|
-
signedOutReason = 'no_session';
|
|
461
|
-
}
|
|
462
|
-
await revert();
|
|
463
|
-
return { kind: 'skip' };
|
|
413
|
+
if (result.status === 'no-session') signedOutReason = 'no_session';
|
|
414
|
+
return {kind: 'skip'};
|
|
464
415
|
},
|
|
465
416
|
});
|
|
466
417
|
}
|
|
@@ -488,8 +439,10 @@ export async function runSessionColdBoot(
|
|
|
488
439
|
id: 'commons-proof-signin',
|
|
489
440
|
enabled: () => isNative && !isOffline(),
|
|
490
441
|
run: async () => {
|
|
491
|
-
|
|
492
|
-
|
|
442
|
+
if (accountRecoverySuperseded || await store.isAutomaticIdentitySignInSuppressed?.()) return { kind: 'skip' };
|
|
443
|
+
const epoch = oxy.http.getSessionEpoch();
|
|
444
|
+
const session = await oxy.auth.signInWithCommonsIdentity({ plantTokens: false, requestOptions: { retry: false } });
|
|
445
|
+
if (!session?.accessToken || oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
493
446
|
return { kind: 'skip' };
|
|
494
447
|
}
|
|
495
448
|
// `verifyChallenge` issues a deviceSecret; persist it so the next
|
|
@@ -505,6 +458,8 @@ export async function runSessionColdBoot(
|
|
|
505
458
|
expiresAt: session.expiresAt,
|
|
506
459
|
});
|
|
507
460
|
}
|
|
461
|
+
if (oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.()) return { kind: 'skip' };
|
|
462
|
+
oxy.session.setAccessToken(session.accessToken);
|
|
508
463
|
return {
|
|
509
464
|
kind: 'session',
|
|
510
465
|
session: {
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export {
|
|
|
40
40
|
ServiceAssetMetadataError,
|
|
41
41
|
} from './OxyServices.errors';
|
|
42
42
|
export type { HttpMethod } from './client/context';
|
|
43
|
-
export type { RequestOptions, AuthenticatedResponseRequest, AuthRefreshReason, AuthRefreshHandler } from './HttpService';
|
|
43
|
+
export type { RequestOptions, ResponseRequest, ResponseTransport, AuthenticatedResponseRequest, AuthRefreshReason, AuthRefreshHandler } from './HttpService';
|
|
44
44
|
|
|
45
45
|
// The namespaces — their classes (for typing) and their input/output types.
|
|
46
46
|
export type { SessionApi, DeviceCredentialProvider, SessionValidation } from './api/session';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MeteredGeneration } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* The Oxy inference client — one surface, two credential lanes (issue #972,
|
|
3
4
|
* workstream 15).
|
|
@@ -50,6 +51,8 @@
|
|
|
50
51
|
* request.
|
|
51
52
|
*/
|
|
52
53
|
|
|
54
|
+
import { decisionRequestSchema, decisionSuccessSchema, decisionAnswersMatch, type DecisionRequest, type DecisionSuccess } from '@oxy.so/contracts';
|
|
55
|
+
|
|
53
56
|
import type {
|
|
54
57
|
CurrencyCode,
|
|
55
58
|
ExactDecimal,
|
|
@@ -261,6 +264,9 @@ export interface OxyGenerationReceipt {
|
|
|
261
264
|
readonly settledAt: string;
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
/** The endpoint returns either a customer receipt or internal technical usage. */
|
|
268
|
+
export type OxyGenerationRecord = OxyGenerationReceipt | MeteredGeneration;
|
|
269
|
+
|
|
264
270
|
/**
|
|
265
271
|
* Anything the inference API refused.
|
|
266
272
|
*
|
|
@@ -471,6 +477,27 @@ export class OxyInferenceClient {
|
|
|
471
477
|
});
|
|
472
478
|
}
|
|
473
479
|
|
|
480
|
+
/** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
|
|
481
|
+
async decide(
|
|
482
|
+
request: DecisionRequest,
|
|
483
|
+
options: OxyInferenceRequestOptions = {},
|
|
484
|
+
): Promise<DecisionSuccess> {
|
|
485
|
+
const input = decisionRequestSchema.parse(request);
|
|
486
|
+
const payload = await this.#request<unknown>('POST', '/v1/decisions', {
|
|
487
|
+
body: input,
|
|
488
|
+
bindRequestId: true,
|
|
489
|
+
signal: options.signal,
|
|
490
|
+
idempotencyKey: options.idempotencyKey,
|
|
491
|
+
delegatedUserId: options.delegatedUserId,
|
|
492
|
+
});
|
|
493
|
+
const parsed = decisionSuccessSchema.safeParse(payload);
|
|
494
|
+
if (!parsed.success || parsed.data.model !== input.model ||
|
|
495
|
+
!decisionAnswersMatch(input, parsed.data.data)) {
|
|
496
|
+
throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
|
|
497
|
+
}
|
|
498
|
+
return parsed.data;
|
|
499
|
+
}
|
|
500
|
+
|
|
474
501
|
/** Generate audio bytes through Oxy's authenticated inference edge. */
|
|
475
502
|
async speech(
|
|
476
503
|
request: OxySpeechRequest,
|
|
@@ -665,12 +692,22 @@ export class OxyInferenceClient {
|
|
|
665
692
|
*/
|
|
666
693
|
async getGeneration(
|
|
667
694
|
id: string,
|
|
668
|
-
options: { signal?: AbortSignal } = {},
|
|
695
|
+
options: { signal?: AbortSignal; delegatedUserId?: string } = {},
|
|
669
696
|
): Promise<OxyGenerationReceipt> {
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
697
|
+
const record = await this.getGenerationRecord(id, options);
|
|
698
|
+
if (record.schemaVersion !== 1) {
|
|
699
|
+
throw new OxyInferenceProtocolError('Internal usage has no financial receipt. Use getGenerationRecord().', record.requestId);
|
|
700
|
+
}
|
|
701
|
+
return record;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Read a financial receipt (v1) or an internal technical record (v2). */
|
|
705
|
+
async getGenerationRecord(
|
|
706
|
+
id: string,
|
|
707
|
+
options: { signal?: AbortSignal; delegatedUserId?: string } = {},
|
|
708
|
+
): Promise<OxyGenerationRecord> {
|
|
709
|
+
const body = await this.#request<{ data: OxyGenerationRecord }>(
|
|
710
|
+
'GET', `/v1/generations/${encodeURIComponent(id)}`, options,
|
|
674
711
|
);
|
|
675
712
|
return body.data;
|
|
676
713
|
}
|
|
@@ -728,6 +765,7 @@ export class OxyInferenceClient {
|
|
|
728
765
|
path: string,
|
|
729
766
|
options: {
|
|
730
767
|
body?: unknown;
|
|
768
|
+
bindRequestId?: boolean;
|
|
731
769
|
signal?: AbortSignal;
|
|
732
770
|
idempotencyKey?: string;
|
|
733
771
|
delegatedUserId?: string;
|
|
@@ -758,6 +796,13 @@ export class OxyInferenceClient {
|
|
|
758
796
|
);
|
|
759
797
|
}
|
|
760
798
|
|
|
799
|
+
if (options.bindRequestId) {
|
|
800
|
+
const requestId = response.headers.get('X-Oxy-Request-Id');
|
|
801
|
+
if (!requestId || typeof payload !== 'object' || payload === null ||
|
|
802
|
+
!('requestId' in payload) || payload.requestId !== requestId) {
|
|
803
|
+
throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
761
806
|
return payload as T;
|
|
762
807
|
}
|
|
763
808
|
}
|