@oxy.so/core 4.4.0 → 4.5.0-oxy-one.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/api/assets.js +28 -1
- package/dist/cjs/api/auth.js +9 -3
- package/dist/cjs/api/billing.js +19 -10
- package/dist/cjs/api/users.js +2 -2
- package/dist/cjs/server/OxyServer.js +21 -0
- package/dist/esm/api/assets.js +28 -1
- package/dist/esm/api/auth.js +9 -3
- package/dist/esm/api/billing.js +15 -6
- package/dist/esm/api/users.js +2 -2
- package/dist/esm/server/OxyServer.js +21 -0
- package/dist/types/api/assets.d.ts +5 -1
- package/dist/types/api/auth.d.ts +8 -3
- package/dist/types/api/billing.d.ts +8 -3
- package/dist/types/api/users.d.ts +3 -1
- package/dist/types/models/interfaces.d.ts +11 -0
- package/dist/types/server/OxyServer.d.ts +5 -0
- package/package.json +2 -2
- package/src/api/assets.ts +29 -1
- package/src/api/auth.ts +11 -4
- package/src/api/billing.ts +19 -6
- package/src/api/users.ts +2 -2
- package/src/models/interfaces.ts +6 -0
- package/src/server/OxyServer.ts +26 -0
package/dist/cjs/api/assets.js
CHANGED
|
@@ -38,6 +38,12 @@ const ASSET_URL_CACHE_LIFETIME_FRACTION = 0.5;
|
|
|
38
38
|
/** Assumed URL lifetime (seconds) when no `expiresIn` is asked for; the API default. */
|
|
39
39
|
const DEFAULT_ASSET_URL_EXPIRES_IN_SECONDS = 3600;
|
|
40
40
|
const ASSET_TTL = 5 * 60 * 1000;
|
|
41
|
+
/**
|
|
42
|
+
* Default bound on one `POST /assets/upload`, in ms. The client's per-request
|
|
43
|
+
* default (5s) is sized for JSON calls; a video sent over a phone connection
|
|
44
|
+
* needs minutes, and timing it out at 5s failed every upload past a few MB.
|
|
45
|
+
*/
|
|
46
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
41
47
|
/**
|
|
42
48
|
* How long a resolved asset URL may stay in the SDK's GET cache, in ms: never
|
|
43
49
|
* longer than the media token's lifetime, and discounted below it.
|
|
@@ -51,6 +57,24 @@ class AssetsApi {
|
|
|
51
57
|
constructor(ctx) {
|
|
52
58
|
this.ctx = ctx;
|
|
53
59
|
}
|
|
60
|
+
/** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
|
|
61
|
+
async putPresignedUpload(init, body) {
|
|
62
|
+
if (!init.uploadUrl)
|
|
63
|
+
return; // bytes already exist for this account
|
|
64
|
+
const target = new URL(init.uploadUrl);
|
|
65
|
+
if (target.protocol !== 'https:')
|
|
66
|
+
throw new Error('Presigned uploads require HTTPS');
|
|
67
|
+
const headers = new Headers(init.requiredHeaders);
|
|
68
|
+
headers.forEach((_value, name) => {
|
|
69
|
+
if (!['if-none-match', 'content-type', 'x-amz-checksum-sha256', 'cache-control'].includes(name.toLowerCase()))
|
|
70
|
+
throw new Error('Unsupported required upload header');
|
|
71
|
+
});
|
|
72
|
+
if (body.type && !headers.has('Content-Type'))
|
|
73
|
+
headers.set('Content-Type', body.type);
|
|
74
|
+
const result = await fetch(target.toString(), { method: 'PUT', body, headers, credentials: 'omit', redirect: 'error' });
|
|
75
|
+
if (!result.ok)
|
|
76
|
+
throw new Error(`Presigned upload failed (${result.status})`);
|
|
77
|
+
}
|
|
54
78
|
// ── Upload ───────────────────────────────────────────────────────────────
|
|
55
79
|
/**
|
|
56
80
|
* Upload a file. Accepts a web `File`/`Blob` or a React Native
|
|
@@ -96,7 +120,10 @@ class AssetsApi {
|
|
|
96
120
|
if (options.metadata)
|
|
97
121
|
formData.append('metadata', JSON.stringify(options.metadata));
|
|
98
122
|
try {
|
|
99
|
-
const response = await this.ctx.request('POST', '/assets/upload', formData, {
|
|
123
|
+
const response = await this.ctx.request('POST', '/assets/upload', formData, {
|
|
124
|
+
cache: false,
|
|
125
|
+
timeout: options.timeout ?? ASSET_UPLOAD_TIMEOUT_MS,
|
|
126
|
+
});
|
|
100
127
|
options.onProgress?.(100);
|
|
101
128
|
return response;
|
|
102
129
|
}
|
package/dist/cjs/api/auth.js
CHANGED
|
@@ -714,14 +714,19 @@ class AuthApi {
|
|
|
714
714
|
this.agent = new AuthAgentApi(ctx);
|
|
715
715
|
}
|
|
716
716
|
/**
|
|
717
|
-
* Register a new
|
|
718
|
-
*
|
|
717
|
+
* Register a new account for a public key, together with its username.
|
|
718
|
+
*
|
|
719
|
+
* The username is required: the server writes it in the same insert as the
|
|
720
|
+
* key, so an account never exists without one. A taken username is a 409
|
|
721
|
+
* (`Username already taken`), as is a key that is already registered
|
|
722
|
+
* (`Identity already registered`).
|
|
719
723
|
*
|
|
720
724
|
* @param publicKey - The user's ECDSA public key (hex)
|
|
721
725
|
* @param signature - Signature of the registration request
|
|
722
726
|
* @param timestamp - Timestamp when the signature was created
|
|
727
|
+
* @param username - The account's username
|
|
723
728
|
*/
|
|
724
|
-
async registerKey(publicKey, signature, timestamp) {
|
|
729
|
+
async registerKey(publicKey, signature, timestamp, username) {
|
|
725
730
|
// Advisory for now (server soft-enforces — see
|
|
726
731
|
// `SessionController.register`), but solved unconditionally so every
|
|
727
732
|
// client is already sending it once the server starts requiring it.
|
|
@@ -731,6 +736,7 @@ class AuthApi {
|
|
|
731
736
|
publicKey,
|
|
732
737
|
signature,
|
|
733
738
|
timestamp,
|
|
739
|
+
username,
|
|
734
740
|
powNonce,
|
|
735
741
|
}, { cache: false, skipAuth: true });
|
|
736
742
|
if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
|
package/dist/cjs/api/billing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BillingApi = void 0;
|
|
4
|
+
const contracts_1 = require("@oxy.so/contracts");
|
|
4
5
|
/**
|
|
5
6
|
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
6
7
|
*
|
|
@@ -12,35 +13,43 @@ exports.BillingApi = void 0;
|
|
|
12
13
|
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
13
14
|
* arrive as ISO-8601 strings.
|
|
14
15
|
*/
|
|
15
|
-
const contracts_1 = require("@oxy.so/contracts");
|
|
16
16
|
const contracts_2 = require("@oxy.so/contracts");
|
|
17
|
+
const contracts_3 = require("@oxy.so/contracts");
|
|
18
|
+
const contracts_4 = require("@oxy.so/contracts");
|
|
17
19
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
18
20
|
const WALLET_TTL = 60 * 1000;
|
|
19
21
|
class BillingApi {
|
|
20
22
|
constructor(ctx) {
|
|
21
23
|
this.ctx = ctx;
|
|
22
24
|
}
|
|
25
|
+
async startPersonalPlanCheckout(request) {
|
|
26
|
+
return contracts_1.personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/personal-plan', contracts_1.personalPlanCheckoutRequestSchema.parse(request), { cache: false }));
|
|
27
|
+
}
|
|
28
|
+
/** Public versioned personal plans. Discovery never proves customer access. */
|
|
29
|
+
async personalPlans() {
|
|
30
|
+
return contracts_2.personalPlanCatalogueSchema.parse(await this.ctx.request('GET', '/billing/personal-plans', undefined, { cache: false }));
|
|
31
|
+
}
|
|
23
32
|
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
24
33
|
async productAccess(query) {
|
|
25
|
-
const parsed =
|
|
34
|
+
const parsed = contracts_3.subjectProductAccessQuerySchema.parse(query);
|
|
26
35
|
const response = await this.ctx.request('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}`, undefined, { cache: false });
|
|
27
|
-
return
|
|
36
|
+
return contracts_3.subjectProductAccessSchema.parse(response);
|
|
28
37
|
}
|
|
29
38
|
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
30
|
-
async productSubscriptions() {
|
|
31
|
-
return
|
|
39
|
+
async productSubscriptions(expectedSubjectAccountId) {
|
|
40
|
+
return contracts_4.productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", expectedSubjectAccountId ? `/billing/product-subscriptions?expectedSubjectAccountId=${encodeURIComponent(expectedSubjectAccountId)}` : "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
|
|
32
41
|
}
|
|
33
42
|
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
34
43
|
async creditGrants() {
|
|
35
|
-
return
|
|
44
|
+
return contracts_4.subscriptionCreditGrantsResponseSchema.parse(await this.ctx.request("GET", "/billing/credit-grants", undefined, { cache: false })).grants;
|
|
36
45
|
}
|
|
37
46
|
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
38
|
-
async cancelProductSubscription(sourceId, expectedSubjectAccountId) {
|
|
39
|
-
await this.ctx.request("POST", "/billing/product-subscriptions/cancel",
|
|
47
|
+
async cancelProductSubscription(sourceId, expectedSubjectAccountId, actionId) {
|
|
48
|
+
await this.ctx.request("POST", "/billing/product-subscriptions/cancel", contracts_4.cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId ? { actionId } : {}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
|
|
40
49
|
}
|
|
41
50
|
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
42
|
-
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId) {
|
|
43
|
-
return
|
|
51
|
+
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId, actionId) {
|
|
52
|
+
return contracts_4.productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', contracts_4.cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId ? { actionId } : {}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
|
|
44
53
|
}
|
|
45
54
|
/** The signed-in user's payment history. Never cached. */
|
|
46
55
|
async payments() {
|
package/dist/cjs/api/users.js
CHANGED
|
@@ -109,8 +109,8 @@ class UsersApi {
|
|
|
109
109
|
return settled.flat();
|
|
110
110
|
}
|
|
111
111
|
/** The signed-in user. Cached 1 minute. */
|
|
112
|
-
async me() {
|
|
113
|
-
const user = await this.ctx.request('GET', '/users/me', undefined, { cache: true, cacheTTL: ME_TTL });
|
|
112
|
+
async me(options) {
|
|
113
|
+
const user = await this.ctx.request('GET', '/users/me', undefined, { cache: options?.cache ?? true, cacheTTL: ME_TTL });
|
|
114
114
|
return (0, userIdentity_1.normalizeUserIdentity)(user);
|
|
115
115
|
}
|
|
116
116
|
/** A profile by username. Cached 5 minutes; `{ cache: false }` as in `get`. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServer = exports.ServiceCredentialMismatchError = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = void 0;
|
|
4
|
+
const contracts_1 = require("@oxy.so/contracts");
|
|
4
5
|
/**
|
|
5
6
|
* `OxyServer` — the Oxy client for a backend.
|
|
6
7
|
*
|
|
@@ -279,6 +280,26 @@ class OxyServer extends OxyServices_1.OxyServices {
|
|
|
279
280
|
* request is on behalf of that user (the API checks the delegation grant).
|
|
280
281
|
* Never cached unless `cache` says so. Rejects with `OxyApiError`.
|
|
281
282
|
*/
|
|
283
|
+
/** Validated request's user session only; never changes the shared client's token. */
|
|
284
|
+
async productGrantSnapshotForUser(query, accessToken) {
|
|
285
|
+
const parsed = contracts_1.subjectProductAccessQuerySchema.parse(query);
|
|
286
|
+
if (!accessToken || /[\r\n]/.test(accessToken))
|
|
287
|
+
throw new Error('A user session is required');
|
|
288
|
+
const answer = await this.request('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/grants`, undefined, { cache: false, retry: false, headers: { Authorization: `Bearer ${accessToken}` } });
|
|
289
|
+
const snapshot = contracts_1.subjectProductGrantSnapshotSchema.parse(answer);
|
|
290
|
+
if (snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
291
|
+
throw new Error('Product grant response attribution differs');
|
|
292
|
+
return snapshot;
|
|
293
|
+
}
|
|
294
|
+
/** Offline metering read: the API requires this app's live consent for this exact subject. */
|
|
295
|
+
async productGrantSnapshotForService(query) {
|
|
296
|
+
const parsed = contracts_1.subjectProductAccessQuerySchema.parse(query);
|
|
297
|
+
const answer = await this.serviceRequest('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/service-grants`, undefined, { cache: false, retry: false });
|
|
298
|
+
const snapshot = contracts_1.subjectProductGrantSnapshotSchema.parse(answer);
|
|
299
|
+
if (snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
300
|
+
throw new Error('Product grant response attribution differs');
|
|
301
|
+
return snapshot;
|
|
302
|
+
}
|
|
282
303
|
async serviceRequest(method, url, data, options = {}) {
|
|
283
304
|
const { actAs, headers, ...rest } = options;
|
|
284
305
|
const token = await this.serviceToken();
|
package/dist/esm/api/assets.js
CHANGED
|
@@ -34,6 +34,12 @@ const ASSET_URL_CACHE_LIFETIME_FRACTION = 0.5;
|
|
|
34
34
|
/** Assumed URL lifetime (seconds) when no `expiresIn` is asked for; the API default. */
|
|
35
35
|
const DEFAULT_ASSET_URL_EXPIRES_IN_SECONDS = 3600;
|
|
36
36
|
const ASSET_TTL = 5 * 60 * 1000;
|
|
37
|
+
/**
|
|
38
|
+
* Default bound on one `POST /assets/upload`, in ms. The client's per-request
|
|
39
|
+
* default (5s) is sized for JSON calls; a video sent over a phone connection
|
|
40
|
+
* needs minutes, and timing it out at 5s failed every upload past a few MB.
|
|
41
|
+
*/
|
|
42
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
37
43
|
/**
|
|
38
44
|
* How long a resolved asset URL may stay in the SDK's GET cache, in ms: never
|
|
39
45
|
* longer than the media token's lifetime, and discounted below it.
|
|
@@ -47,6 +53,24 @@ export class AssetsApi {
|
|
|
47
53
|
constructor(ctx) {
|
|
48
54
|
this.ctx = ctx;
|
|
49
55
|
}
|
|
56
|
+
/** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
|
|
57
|
+
async putPresignedUpload(init, body) {
|
|
58
|
+
if (!init.uploadUrl)
|
|
59
|
+
return; // bytes already exist for this account
|
|
60
|
+
const target = new URL(init.uploadUrl);
|
|
61
|
+
if (target.protocol !== 'https:')
|
|
62
|
+
throw new Error('Presigned uploads require HTTPS');
|
|
63
|
+
const headers = new Headers(init.requiredHeaders);
|
|
64
|
+
headers.forEach((_value, name) => {
|
|
65
|
+
if (!['if-none-match', 'content-type', 'x-amz-checksum-sha256', 'cache-control'].includes(name.toLowerCase()))
|
|
66
|
+
throw new Error('Unsupported required upload header');
|
|
67
|
+
});
|
|
68
|
+
if (body.type && !headers.has('Content-Type'))
|
|
69
|
+
headers.set('Content-Type', body.type);
|
|
70
|
+
const result = await fetch(target.toString(), { method: 'PUT', body, headers, credentials: 'omit', redirect: 'error' });
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
throw new Error(`Presigned upload failed (${result.status})`);
|
|
73
|
+
}
|
|
50
74
|
// ── Upload ───────────────────────────────────────────────────────────────
|
|
51
75
|
/**
|
|
52
76
|
* Upload a file. Accepts a web `File`/`Blob` or a React Native
|
|
@@ -92,7 +116,10 @@ export class AssetsApi {
|
|
|
92
116
|
if (options.metadata)
|
|
93
117
|
formData.append('metadata', JSON.stringify(options.metadata));
|
|
94
118
|
try {
|
|
95
|
-
const response = await this.ctx.request('POST', '/assets/upload', formData, {
|
|
119
|
+
const response = await this.ctx.request('POST', '/assets/upload', formData, {
|
|
120
|
+
cache: false,
|
|
121
|
+
timeout: options.timeout ?? ASSET_UPLOAD_TIMEOUT_MS,
|
|
122
|
+
});
|
|
96
123
|
options.onProgress?.(100);
|
|
97
124
|
return response;
|
|
98
125
|
}
|
package/dist/esm/api/auth.js
CHANGED
|
@@ -672,14 +672,19 @@ export class AuthApi {
|
|
|
672
672
|
this.agent = new AuthAgentApi(ctx);
|
|
673
673
|
}
|
|
674
674
|
/**
|
|
675
|
-
* Register a new
|
|
676
|
-
*
|
|
675
|
+
* Register a new account for a public key, together with its username.
|
|
676
|
+
*
|
|
677
|
+
* The username is required: the server writes it in the same insert as the
|
|
678
|
+
* key, so an account never exists without one. A taken username is a 409
|
|
679
|
+
* (`Username already taken`), as is a key that is already registered
|
|
680
|
+
* (`Identity already registered`).
|
|
677
681
|
*
|
|
678
682
|
* @param publicKey - The user's ECDSA public key (hex)
|
|
679
683
|
* @param signature - Signature of the registration request
|
|
680
684
|
* @param timestamp - Timestamp when the signature was created
|
|
685
|
+
* @param username - The account's username
|
|
681
686
|
*/
|
|
682
|
-
async registerKey(publicKey, signature, timestamp) {
|
|
687
|
+
async registerKey(publicKey, signature, timestamp, username) {
|
|
683
688
|
// Advisory for now (server soft-enforces — see
|
|
684
689
|
// `SessionController.register`), but solved unconditionally so every
|
|
685
690
|
// client is already sending it once the server starts requiring it.
|
|
@@ -689,6 +694,7 @@ export class AuthApi {
|
|
|
689
694
|
publicKey,
|
|
690
695
|
signature,
|
|
691
696
|
timestamp,
|
|
697
|
+
username,
|
|
692
698
|
powNonce,
|
|
693
699
|
}, { cache: false, skipAuth: true });
|
|
694
700
|
if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
|
package/dist/esm/api/billing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personalPlanCheckoutRequestSchema, personalPlanCheckoutResultSchema } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
3
4
|
*
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
10
11
|
* arrive as ISO-8601 strings.
|
|
11
12
|
*/
|
|
13
|
+
import { personalPlanCatalogueSchema } from '@oxy.so/contracts';
|
|
12
14
|
import { subjectProductAccessQuerySchema, subjectProductAccessSchema, } from '@oxy.so/contracts';
|
|
13
15
|
import { productSubscriptionCancellationResultSchema, cancelProductSubscriptionSchema, productSubscriptionsResponseSchema, subscriptionCreditGrantsResponseSchema, } from '@oxy.so/contracts';
|
|
14
16
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
@@ -17,6 +19,13 @@ export class BillingApi {
|
|
|
17
19
|
constructor(ctx) {
|
|
18
20
|
this.ctx = ctx;
|
|
19
21
|
}
|
|
22
|
+
async startPersonalPlanCheckout(request) {
|
|
23
|
+
return personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/personal-plan', personalPlanCheckoutRequestSchema.parse(request), { cache: false }));
|
|
24
|
+
}
|
|
25
|
+
/** Public versioned personal plans. Discovery never proves customer access. */
|
|
26
|
+
async personalPlans() {
|
|
27
|
+
return personalPlanCatalogueSchema.parse(await this.ctx.request('GET', '/billing/personal-plans', undefined, { cache: false }));
|
|
28
|
+
}
|
|
20
29
|
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
21
30
|
async productAccess(query) {
|
|
22
31
|
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
@@ -24,20 +33,20 @@ export class BillingApi {
|
|
|
24
33
|
return subjectProductAccessSchema.parse(response);
|
|
25
34
|
}
|
|
26
35
|
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
27
|
-
async productSubscriptions() {
|
|
28
|
-
return productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
|
|
36
|
+
async productSubscriptions(expectedSubjectAccountId) {
|
|
37
|
+
return productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", expectedSubjectAccountId ? `/billing/product-subscriptions?expectedSubjectAccountId=${encodeURIComponent(expectedSubjectAccountId)}` : "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
|
|
29
38
|
}
|
|
30
39
|
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
31
40
|
async creditGrants() {
|
|
32
41
|
return subscriptionCreditGrantsResponseSchema.parse(await this.ctx.request("GET", "/billing/credit-grants", undefined, { cache: false })).grants;
|
|
33
42
|
}
|
|
34
43
|
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
35
|
-
async cancelProductSubscription(sourceId, expectedSubjectAccountId) {
|
|
36
|
-
await this.ctx.request("POST", "/billing/product-subscriptions/cancel", cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
|
|
44
|
+
async cancelProductSubscription(sourceId, expectedSubjectAccountId, actionId) {
|
|
45
|
+
await this.ctx.request("POST", "/billing/product-subscriptions/cancel", cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId ? { actionId } : {}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
|
|
37
46
|
}
|
|
38
47
|
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
39
|
-
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId) {
|
|
40
|
-
return productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
|
|
48
|
+
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId, actionId) {
|
|
49
|
+
return productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId ? { actionId } : {}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
|
|
41
50
|
}
|
|
42
51
|
/** The signed-in user's payment history. Never cached. */
|
|
43
52
|
async payments() {
|
package/dist/esm/api/users.js
CHANGED
|
@@ -73,8 +73,8 @@ export class UsersApi {
|
|
|
73
73
|
return settled.flat();
|
|
74
74
|
}
|
|
75
75
|
/** The signed-in user. Cached 1 minute. */
|
|
76
|
-
async me() {
|
|
77
|
-
const user = await this.ctx.request('GET', '/users/me', undefined, { cache: true, cacheTTL: ME_TTL });
|
|
76
|
+
async me(options) {
|
|
77
|
+
const user = await this.ctx.request('GET', '/users/me', undefined, { cache: options?.cache ?? true, cacheTTL: ME_TTL });
|
|
78
78
|
return normalizeUserIdentity(user);
|
|
79
79
|
}
|
|
80
80
|
/** A profile by username. Cached 5 minutes; `{ cache: false }` as in `get`. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { subjectProductAccessQuerySchema, subjectProductGrantSnapshotSchema } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* `OxyServer` — the Oxy client for a backend.
|
|
3
4
|
*
|
|
@@ -275,6 +276,26 @@ export class OxyServer extends OxyServices {
|
|
|
275
276
|
* request is on behalf of that user (the API checks the delegation grant).
|
|
276
277
|
* Never cached unless `cache` says so. Rejects with `OxyApiError`.
|
|
277
278
|
*/
|
|
279
|
+
/** Validated request's user session only; never changes the shared client's token. */
|
|
280
|
+
async productGrantSnapshotForUser(query, accessToken) {
|
|
281
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
282
|
+
if (!accessToken || /[\r\n]/.test(accessToken))
|
|
283
|
+
throw new Error('A user session is required');
|
|
284
|
+
const answer = await this.request('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/grants`, undefined, { cache: false, retry: false, headers: { Authorization: `Bearer ${accessToken}` } });
|
|
285
|
+
const snapshot = subjectProductGrantSnapshotSchema.parse(answer);
|
|
286
|
+
if (snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
287
|
+
throw new Error('Product grant response attribution differs');
|
|
288
|
+
return snapshot;
|
|
289
|
+
}
|
|
290
|
+
/** Offline metering read: the API requires this app's live consent for this exact subject. */
|
|
291
|
+
async productGrantSnapshotForService(query) {
|
|
292
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
293
|
+
const answer = await this.serviceRequest('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/service-grants`, undefined, { cache: false, retry: false });
|
|
294
|
+
const snapshot = subjectProductGrantSnapshotSchema.parse(answer);
|
|
295
|
+
if (snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
296
|
+
throw new Error('Product grant response attribution differs');
|
|
297
|
+
return snapshot;
|
|
298
|
+
}
|
|
278
299
|
async serviceRequest(method, url, data, options = {}) {
|
|
279
300
|
const { actAs, headers, ...rest } = options;
|
|
280
301
|
const token = await this.serviceToken();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OxyContext } from '../client/context';
|
|
2
|
-
import type { AccountStorageUsageResponse, Asset, AssetDeleteSummary, AssetLink, AssetMetadata, AssetUploadInput, BatchFileAccessResponse, FileVisibility } from '../models/interfaces';
|
|
2
|
+
import type { AccountStorageUsageResponse, AssetInitResponse, Asset, AssetDeleteSummary, AssetLink, AssetMetadata, AssetUploadInput, BatchFileAccessResponse, FileVisibility } from '../models/interfaces';
|
|
3
3
|
/**
|
|
4
4
|
* How long a resolved asset URL may stay in the SDK's GET cache, in ms: never
|
|
5
5
|
* longer than the media token's lifetime, and discounted below it.
|
|
@@ -27,6 +27,8 @@ export interface AssetUploadOptions {
|
|
|
27
27
|
metadata?: AssetMetadata;
|
|
28
28
|
/** Called with `100` once the upload completes. */
|
|
29
29
|
onProgress?: (progress: number) => void;
|
|
30
|
+
/** Bound on the whole upload, in ms. Default {@link ASSET_UPLOAD_TIMEOUT_MS}. */
|
|
31
|
+
timeout?: number;
|
|
30
32
|
}
|
|
31
33
|
export interface AssetLinkTarget {
|
|
32
34
|
app: string;
|
|
@@ -41,6 +43,8 @@ export interface AssetDeleteResult {
|
|
|
41
43
|
export declare class AssetsApi {
|
|
42
44
|
protected readonly ctx: OxyContext;
|
|
43
45
|
constructor(ctx: OxyContext);
|
|
46
|
+
/** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
|
|
47
|
+
putPresignedUpload(init: AssetInitResponse, body: Blob): Promise<void>;
|
|
44
48
|
/**
|
|
45
49
|
* Upload a file. Accepts a web `File`/`Blob` or a React Native
|
|
46
50
|
* `{ uri, type?, name?, size? }` descriptor.
|
package/dist/types/api/auth.d.ts
CHANGED
|
@@ -633,14 +633,19 @@ export declare class AuthApi {
|
|
|
633
633
|
readonly agent: AuthAgentApi;
|
|
634
634
|
constructor(ctx: OxyContext);
|
|
635
635
|
/**
|
|
636
|
-
* Register a new
|
|
637
|
-
*
|
|
636
|
+
* Register a new account for a public key, together with its username.
|
|
637
|
+
*
|
|
638
|
+
* The username is required: the server writes it in the same insert as the
|
|
639
|
+
* key, so an account never exists without one. A taken username is a 409
|
|
640
|
+
* (`Username already taken`), as is a key that is already registered
|
|
641
|
+
* (`Identity already registered`).
|
|
638
642
|
*
|
|
639
643
|
* @param publicKey - The user's ECDSA public key (hex)
|
|
640
644
|
* @param signature - Signature of the registration request
|
|
641
645
|
* @param timestamp - Timestamp when the signature was created
|
|
646
|
+
* @param username - The account's username
|
|
642
647
|
*/
|
|
643
|
-
registerKey(publicKey: string, signature: string, timestamp: number): Promise<{
|
|
648
|
+
registerKey(publicKey: string, signature: string, timestamp: number, username: string): Promise<{
|
|
644
649
|
message: string;
|
|
645
650
|
user: User;
|
|
646
651
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PersonalPlanCheckoutRequest, type PersonalPlanCheckoutResult } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
3
4
|
*
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
10
11
|
* arrive as ISO-8601 strings.
|
|
11
12
|
*/
|
|
13
|
+
import { type PersonalPlanCatalogue } from '@oxy.so/contracts';
|
|
12
14
|
import { type SubjectProductAccess, type SubjectProductAccessQuery } from '@oxy.so/contracts';
|
|
13
15
|
import { type ProductSubscriptionSummary, type SubscriptionCreditGrant, type ProductSubscriptionCancellationResult } from '@oxy.so/contracts';
|
|
14
16
|
import type { OxyContext } from '../client/context';
|
|
@@ -99,16 +101,19 @@ export interface WalletTransactionsPage {
|
|
|
99
101
|
export declare class BillingApi {
|
|
100
102
|
protected readonly ctx: OxyContext;
|
|
101
103
|
constructor(ctx: OxyContext);
|
|
104
|
+
startPersonalPlanCheckout(request: PersonalPlanCheckoutRequest): Promise<PersonalPlanCheckoutResult>;
|
|
105
|
+
/** Public versioned personal plans. Discovery never proves customer access. */
|
|
106
|
+
personalPlans(): Promise<PersonalPlanCatalogue>;
|
|
102
107
|
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
103
108
|
productAccess(query: SubjectProductAccessQuery): Promise<SubjectProductAccess>;
|
|
104
109
|
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
105
|
-
productSubscriptions(): Promise<ProductSubscriptionSummary[]>;
|
|
110
|
+
productSubscriptions(expectedSubjectAccountId?: string): Promise<ProductSubscriptionSummary[]>;
|
|
106
111
|
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
107
112
|
creditGrants(): Promise<SubscriptionCreditGrant[]>;
|
|
108
113
|
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
109
|
-
cancelProductSubscription(sourceId: string, expectedSubjectAccountId?: string): Promise<void>;
|
|
114
|
+
cancelProductSubscription(sourceId: string, expectedSubjectAccountId?: string, actionId?: string): Promise<void>;
|
|
110
115
|
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
111
|
-
cancelProductSubscriptionWithStatus(sourceId: string, expectedSubjectAccountId?: string): Promise<ProductSubscriptionCancellationResult>;
|
|
116
|
+
cancelProductSubscriptionWithStatus(sourceId: string, expectedSubjectAccountId?: string, actionId?: string): Promise<ProductSubscriptionCancellationResult>;
|
|
112
117
|
/** The signed-in user's payment history. Never cached. */
|
|
113
118
|
payments(): Promise<Payment[]>;
|
|
114
119
|
/** A user's subscription (default: the signed-in user's). */
|
|
@@ -61,7 +61,9 @@ export declare class UsersApi {
|
|
|
61
61
|
*/
|
|
62
62
|
getMany(ids: string[]): Promise<User[]>;
|
|
63
63
|
/** The signed-in user. Cached 1 minute. */
|
|
64
|
-
me(
|
|
64
|
+
me(options?: {
|
|
65
|
+
cache?: boolean;
|
|
66
|
+
}): Promise<User>;
|
|
65
67
|
/** A profile by username. Cached 5 minutes; `{ cache: false }` as in `get`. */
|
|
66
68
|
byUsername(username: string, options?: {
|
|
67
69
|
cache?: boolean;
|
|
@@ -200,6 +200,13 @@ export interface User {
|
|
|
200
200
|
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
201
201
|
*/
|
|
202
202
|
themePreference?: ThemePreference;
|
|
203
|
+
/** Product-specific personalization; never inferred from generic premium. */
|
|
204
|
+
personalization?: {
|
|
205
|
+
mentionMono: {
|
|
206
|
+
allowed: boolean;
|
|
207
|
+
expiresAt: string | null;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
203
210
|
/**
|
|
204
211
|
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
205
212
|
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
@@ -520,6 +527,8 @@ export interface AssetInitRequest {
|
|
|
520
527
|
mime: string;
|
|
521
528
|
}
|
|
522
529
|
export interface AssetInitResponse {
|
|
530
|
+
/** Replay these headers on PUT; Content-Length is supplied by the transport. */
|
|
531
|
+
requiredHeaders?: Record<string, string>;
|
|
523
532
|
uploadUrl: string;
|
|
524
533
|
fileId: string;
|
|
525
534
|
sha256: string;
|
|
@@ -694,6 +703,8 @@ export interface AccountStorageCategoryUsage {
|
|
|
694
703
|
count: number;
|
|
695
704
|
}
|
|
696
705
|
export interface AccountStorageUsageResponse {
|
|
706
|
+
quotaEnforcement?: 'metadata_admission' | 'unconfigured';
|
|
707
|
+
reservedBytes?: string | null;
|
|
697
708
|
plan: 'basic' | 'pro' | 'business';
|
|
698
709
|
totalUsedBytes: number;
|
|
699
710
|
totalLimitBytes: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SubjectProductAccessQuery, type SubjectProductGrantSnapshot } from '@oxy.so/contracts';
|
|
1
2
|
import { OxyServices, type OxyConfig } from '../OxyServices';
|
|
2
3
|
import type { HttpMethod } from '../client/context';
|
|
3
4
|
import type { RequestOptions } from '../HttpService';
|
|
@@ -132,6 +133,10 @@ export declare class OxyServer extends OxyServices {
|
|
|
132
133
|
* request is on behalf of that user (the API checks the delegation grant).
|
|
133
134
|
* Never cached unless `cache` says so. Rejects with `OxyApiError`.
|
|
134
135
|
*/
|
|
136
|
+
/** Validated request's user session only; never changes the shared client's token. */
|
|
137
|
+
productGrantSnapshotForUser(query: SubjectProductAccessQuery, accessToken: string): Promise<SubjectProductGrantSnapshot>;
|
|
138
|
+
/** Offline metering read: the API requires this app's live consent for this exact subject. */
|
|
139
|
+
productGrantSnapshotForService(query: SubjectProductAccessQuery): Promise<SubjectProductGrantSnapshot>;
|
|
135
140
|
serviceRequest<T>(method: HttpMethod, url: string, data?: unknown, options?: RequestOptions & {
|
|
136
141
|
actAs?: string;
|
|
137
142
|
}): Promise<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0-oxy-one.1",
|
|
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",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"@oxy.so/telemetry": "^1.2.0",
|
|
180
180
|
"@noble/ciphers": "^1.3.0",
|
|
181
181
|
"@noble/hashes": "^1.8.0",
|
|
182
|
-
"@oxy.so/contracts": "
|
|
182
|
+
"@oxy.so/contracts": "4.11.0-oxy-one.0",
|
|
183
183
|
"@oxy.so/protocol": "^1.2.2",
|
|
184
184
|
"@scure/bip39": "^1.6.0",
|
|
185
185
|
"buffer": "^6.0.3",
|
package/src/api/assets.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { OxyContext } from '../client/context';
|
|
|
18
18
|
import { logger } from '../logger';
|
|
19
19
|
import type {
|
|
20
20
|
AccountStorageUsageResponse,
|
|
21
|
+
AssetInitResponse,
|
|
21
22
|
Asset,
|
|
22
23
|
AssetDeleteSummary,
|
|
23
24
|
AssetLink,
|
|
@@ -51,6 +52,13 @@ const DEFAULT_ASSET_URL_EXPIRES_IN_SECONDS = 3600;
|
|
|
51
52
|
|
|
52
53
|
const ASSET_TTL = 5 * 60 * 1000;
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Default bound on one `POST /assets/upload`, in ms. The client's per-request
|
|
57
|
+
* default (5s) is sized for JSON calls; a video sent over a phone connection
|
|
58
|
+
* needs minutes, and timing it out at 5s failed every upload past a few MB.
|
|
59
|
+
*/
|
|
60
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
61
|
+
|
|
54
62
|
/**
|
|
55
63
|
* How long a resolved asset URL may stay in the SDK's GET cache, in ms: never
|
|
56
64
|
* longer than the media token's lifetime, and discounted below it.
|
|
@@ -84,6 +92,8 @@ export interface AssetUploadOptions {
|
|
|
84
92
|
metadata?: AssetMetadata;
|
|
85
93
|
/** Called with `100` once the upload completes. */
|
|
86
94
|
onProgress?: (progress: number) => void;
|
|
95
|
+
/** Bound on the whole upload, in ms. Default {@link ASSET_UPLOAD_TIMEOUT_MS}. */
|
|
96
|
+
timeout?: number;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
export interface AssetLinkTarget {
|
|
@@ -101,6 +111,21 @@ export interface AssetDeleteResult {
|
|
|
101
111
|
export class AssetsApi {
|
|
102
112
|
constructor(protected readonly ctx: OxyContext) {}
|
|
103
113
|
|
|
114
|
+
/** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
|
|
115
|
+
async putPresignedUpload(init: AssetInitResponse, body: Blob): Promise<void> {
|
|
116
|
+
if (!init.uploadUrl) return; // bytes already exist for this account
|
|
117
|
+
const target = new URL(init.uploadUrl);
|
|
118
|
+
if (target.protocol !== 'https:') throw new Error('Presigned uploads require HTTPS');
|
|
119
|
+
const headers = new Headers(init.requiredHeaders);
|
|
120
|
+
headers.forEach((_value,name) => {
|
|
121
|
+
if (!['if-none-match','content-type','x-amz-checksum-sha256','cache-control'].includes(name.toLowerCase()))
|
|
122
|
+
throw new Error('Unsupported required upload header');
|
|
123
|
+
});
|
|
124
|
+
if (body.type && !headers.has('Content-Type')) headers.set('Content-Type',body.type);
|
|
125
|
+
const result = await fetch(target.toString(),{method:'PUT',body,headers,credentials:'omit',redirect:'error'});
|
|
126
|
+
if(!result.ok)throw new Error(`Presigned upload failed (${result.status})`);
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
// ── Upload ───────────────────────────────────────────────────────────────
|
|
105
130
|
|
|
106
131
|
/**
|
|
@@ -144,7 +169,10 @@ export class AssetsApi {
|
|
|
144
169
|
if (options.metadata) formData.append('metadata', JSON.stringify(options.metadata));
|
|
145
170
|
|
|
146
171
|
try {
|
|
147
|
-
const response = await this.ctx.request<{ file: UploadedAsset }>('POST', '/assets/upload', formData, {
|
|
172
|
+
const response = await this.ctx.request<{ file: UploadedAsset }>('POST', '/assets/upload', formData, {
|
|
173
|
+
cache: false,
|
|
174
|
+
timeout: options.timeout ?? ASSET_UPLOAD_TIMEOUT_MS,
|
|
175
|
+
});
|
|
148
176
|
options.onProgress?.(100);
|
|
149
177
|
return response;
|
|
150
178
|
} catch (error) {
|
package/src/api/auth.ts
CHANGED
|
@@ -1208,17 +1208,23 @@ export class AuthApi {
|
|
|
1208
1208
|
}
|
|
1209
1209
|
|
|
1210
1210
|
/**
|
|
1211
|
-
* Register a new
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1211
|
+
* Register a new account for a public key, together with its username.
|
|
1212
|
+
*
|
|
1213
|
+
* The username is required: the server writes it in the same insert as the
|
|
1214
|
+
* key, so an account never exists without one. A taken username is a 409
|
|
1215
|
+
* (`Username already taken`), as is a key that is already registered
|
|
1216
|
+
* (`Identity already registered`).
|
|
1217
|
+
*
|
|
1214
1218
|
* @param publicKey - The user's ECDSA public key (hex)
|
|
1215
1219
|
* @param signature - Signature of the registration request
|
|
1216
1220
|
* @param timestamp - Timestamp when the signature was created
|
|
1221
|
+
* @param username - The account's username
|
|
1217
1222
|
*/
|
|
1218
1223
|
async registerKey(
|
|
1219
1224
|
publicKey: string,
|
|
1220
1225
|
signature: string,
|
|
1221
|
-
timestamp: number
|
|
1226
|
+
timestamp: number,
|
|
1227
|
+
username: string
|
|
1222
1228
|
): Promise<{ message: string; user: User }> {
|
|
1223
1229
|
// Advisory for now (server soft-enforces — see
|
|
1224
1230
|
// `SessionController.register`), but solved unconditionally so every
|
|
@@ -1230,6 +1236,7 @@ export class AuthApi {
|
|
|
1230
1236
|
publicKey,
|
|
1231
1237
|
signature,
|
|
1232
1238
|
timestamp,
|
|
1239
|
+
username,
|
|
1233
1240
|
powNonce,
|
|
1234
1241
|
}, { cache: false, skipAuth: true });
|
|
1235
1242
|
|
package/src/api/billing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personalPlanCheckoutRequestSchema, personalPlanCheckoutResultSchema, type PersonalPlanCheckoutRequest, type PersonalPlanCheckoutResult } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
3
4
|
*
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
10
11
|
* arrive as ISO-8601 strings.
|
|
11
12
|
*/
|
|
13
|
+
import { personalPlanCatalogueSchema, type PersonalPlanCatalogue } from '@oxy.so/contracts';
|
|
12
14
|
import { type SubjectProductAccess,
|
|
13
15
|
type SubjectProductAccessQuery,
|
|
14
16
|
subjectProductAccessQuerySchema, subjectProductAccessSchema, } from '@oxy.so/contracts';
|
|
@@ -115,6 +117,17 @@ export interface WalletTransactionsPage {
|
|
|
115
117
|
export class BillingApi {
|
|
116
118
|
constructor(protected readonly ctx: OxyContext) {}
|
|
117
119
|
|
|
120
|
+
async startPersonalPlanCheckout(request: PersonalPlanCheckoutRequest): Promise<PersonalPlanCheckoutResult> {
|
|
121
|
+
return personalPlanCheckoutResultSchema.parse(await this.ctx.request<unknown>('POST', '/billing/checkout/personal-plan',
|
|
122
|
+
personalPlanCheckoutRequestSchema.parse(request), { cache: false }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Public versioned personal plans. Discovery never proves customer access. */
|
|
126
|
+
async personalPlans(): Promise<PersonalPlanCatalogue> {
|
|
127
|
+
return personalPlanCatalogueSchema.parse(await this.ctx.request<unknown>(
|
|
128
|
+
'GET', '/billing/personal-plans', undefined, { cache: false }));
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
119
132
|
async productAccess(query: SubjectProductAccessQuery): Promise<SubjectProductAccess> {
|
|
120
133
|
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
@@ -125,11 +138,11 @@ export class BillingApi {
|
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
128
|
-
async productSubscriptions(): Promise<ProductSubscriptionSummary[]> {
|
|
141
|
+
async productSubscriptions(expectedSubjectAccountId?: string): Promise<ProductSubscriptionSummary[]> {
|
|
129
142
|
return productSubscriptionsResponseSchema.parse(
|
|
130
143
|
await this.ctx.request<unknown>(
|
|
131
144
|
"GET",
|
|
132
|
-
"/billing/product-subscriptions",
|
|
145
|
+
expectedSubjectAccountId ? `/billing/product-subscriptions?expectedSubjectAccountId=${encodeURIComponent(expectedSubjectAccountId)}` : "/billing/product-subscriptions",
|
|
133
146
|
undefined,
|
|
134
147
|
{ cache: false },
|
|
135
148
|
),
|
|
@@ -149,20 +162,20 @@ export class BillingApi {
|
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/** 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> {
|
|
165
|
+
async cancelProductSubscription(sourceId: string, expectedSubjectAccountId?: string, actionId?:string): Promise<void> {
|
|
153
166
|
await this.ctx.request(
|
|
154
167
|
"POST",
|
|
155
168
|
"/billing/product-subscriptions/cancel",
|
|
156
|
-
cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
169
|
+
cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId?{actionId}:{}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
157
170
|
{ cache: false },
|
|
158
171
|
);
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
162
|
-
async cancelProductSubscriptionWithStatus(sourceId: string, expectedSubjectAccountId?: string): Promise<ProductSubscriptionCancellationResult> {
|
|
175
|
+
async cancelProductSubscriptionWithStatus(sourceId: string, expectedSubjectAccountId?: string, actionId?:string): Promise<ProductSubscriptionCancellationResult> {
|
|
163
176
|
return productSubscriptionCancellationResultSchema.parse(await this.ctx.request<unknown>(
|
|
164
177
|
'POST', '/billing/product-subscriptions/cancel',
|
|
165
|
-
cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
178
|
+
cancelProductSubscriptionSchema.parse({ sourceId, ...(actionId?{actionId}:{}), ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }),
|
|
166
179
|
{ cache: false },
|
|
167
180
|
));
|
|
168
181
|
}
|
package/src/api/users.ts
CHANGED
|
@@ -121,8 +121,8 @@ export class UsersApi {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** The signed-in user. Cached 1 minute. */
|
|
124
|
-
async me(): Promise<User> {
|
|
125
|
-
const user = await this.ctx.request<User>('GET', '/users/me', undefined, { cache: true, cacheTTL: ME_TTL });
|
|
124
|
+
async me(options?: {cache?: boolean}): Promise<User> {
|
|
125
|
+
const user = await this.ctx.request<User>('GET', '/users/me', undefined, { cache: options?.cache ?? true, cacheTTL: ME_TTL });
|
|
126
126
|
return normalizeUserIdentity(user);
|
|
127
127
|
}
|
|
128
128
|
|
package/src/models/interfaces.ts
CHANGED
|
@@ -224,6 +224,8 @@ export interface User {
|
|
|
224
224
|
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
225
225
|
*/
|
|
226
226
|
themePreference?: ThemePreference;
|
|
227
|
+
/** Product-specific personalization; never inferred from generic premium. */
|
|
228
|
+
personalization?: {mentionMono:{allowed:boolean;expiresAt:string|null}};
|
|
227
229
|
/**
|
|
228
230
|
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
229
231
|
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
@@ -581,6 +583,8 @@ export interface AssetInitRequest {
|
|
|
581
583
|
}
|
|
582
584
|
|
|
583
585
|
export interface AssetInitResponse {
|
|
586
|
+
/** Replay these headers on PUT; Content-Length is supplied by the transport. */
|
|
587
|
+
requiredHeaders?: Record<string, string>;
|
|
584
588
|
uploadUrl: string;
|
|
585
589
|
fileId: string;
|
|
586
590
|
sha256: string;
|
|
@@ -769,6 +773,8 @@ export interface AccountStorageCategoryUsage {
|
|
|
769
773
|
}
|
|
770
774
|
|
|
771
775
|
export interface AccountStorageUsageResponse {
|
|
776
|
+
quotaEnforcement?: 'metadata_admission' | 'unconfigured';
|
|
777
|
+
reservedBytes?: string | null;
|
|
772
778
|
plan: 'basic' | 'pro' | 'business';
|
|
773
779
|
totalUsedBytes: number;
|
|
774
780
|
totalLimitBytes: number;
|
package/src/server/OxyServer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { subjectProductAccessQuerySchema, subjectProductGrantSnapshotSchema, type SubjectProductAccessQuery, type SubjectProductGrantSnapshot } from '@oxy.so/contracts';
|
|
1
2
|
/**
|
|
2
3
|
* `OxyServer` — the Oxy client for a backend.
|
|
3
4
|
*
|
|
@@ -362,6 +363,31 @@ export class OxyServer extends OxyServices {
|
|
|
362
363
|
* request is on behalf of that user (the API checks the delegation grant).
|
|
363
364
|
* Never cached unless `cache` says so. Rejects with `OxyApiError`.
|
|
364
365
|
*/
|
|
366
|
+
/** Validated request's user session only; never changes the shared client's token. */
|
|
367
|
+
async productGrantSnapshotForUser(query: SubjectProductAccessQuery, accessToken: string): Promise<SubjectProductGrantSnapshot> {
|
|
368
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
369
|
+
if (!accessToken || /[\r\n]/.test(accessToken)) throw new Error('A user session is required');
|
|
370
|
+
const answer = await this.request<unknown>('GET',
|
|
371
|
+
`/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/grants`,
|
|
372
|
+
undefined, { cache: false, retry: false, headers: { Authorization: `Bearer ${accessToken}` } });
|
|
373
|
+
const snapshot = subjectProductGrantSnapshotSchema.parse(answer);
|
|
374
|
+
if(snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
375
|
+
throw new Error('Product grant response attribution differs');
|
|
376
|
+
return snapshot;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Offline metering read: the API requires this app's live consent for this exact subject. */
|
|
380
|
+
async productGrantSnapshotForService(query: SubjectProductAccessQuery): Promise<SubjectProductGrantSnapshot> {
|
|
381
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
382
|
+
const answer = await this.serviceRequest<unknown>('GET',
|
|
383
|
+
`/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}/service-grants`,
|
|
384
|
+
undefined, { cache: false, retry: false });
|
|
385
|
+
const snapshot = subjectProductGrantSnapshotSchema.parse(answer);
|
|
386
|
+
if (snapshot.access.subjectAccountId !== parsed.subjectAccountId || snapshot.access.productId !== parsed.productId)
|
|
387
|
+
throw new Error('Product grant response attribution differs');
|
|
388
|
+
return snapshot;
|
|
389
|
+
}
|
|
390
|
+
|
|
365
391
|
async serviceRequest<T>(
|
|
366
392
|
method: HttpMethod,
|
|
367
393
|
url: string,
|