@oxy.so/core 7.21.1 → 7.23.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/README.md CHANGED
@@ -31,7 +31,7 @@ The package exposes two public entry points:
31
31
  - `@oxy.so/core/session` — `SessionClient`, the account dialog controller, auth-state stores, cold boot
32
32
  - `@oxy.so/core/crypto` — `KeyManager`, `SignatureService`, `RecoveryPhraseService` and the primitives under them
33
33
  - `@oxy.so/core/civic`, `@oxy.so/core/inference`, `@oxy.so/core/logger`
34
- - `@oxy.so/core/server` — `OxyServer` (workload service tokens, `middleware.*`, `withUser` / `asUser`, `appPermissionsFor`, `accountAccessFor`, `applicationAccessFor`) and the Express helpers (`createOxyRateLimit`, `createOxyAuthMiddleware`, `getOxyUserId`, `getRequiredOxyUserId`, `createOxyCors`, `createOxySocketCors`, `createOxySecurityHeaders`, `buildOxyCspDirectives`, `safeFetch`, `verifySecret`, and request types)
34
+ - `@oxy.so/core/server` — `OxyServer` (workload service tokens, `middleware.*`, `withUser` / `asUser`, `isFirstPartyServiceUrl`, `appPermissionsFor`, `accountAccessFor`, `applicationAccessFor`) and the Express helpers (`createOxyRateLimit`, `createOxyAuthMiddleware`, `getOxyUserId`, `getRequiredOxyUserId`, `createOxyCors`, `createOxySocketCors`, `createOxySecurityHeaders`, `buildOxyCspDirectives`, `safeFetch`, `verifySecret`, and request types)
35
35
 
36
36
  Heavy pieces have their own entries so an app ships only what it uses; React Native and browser bundles never import Express. The full API map is [docs/api.mdx](./docs/api.mdx).
37
37
 
@@ -20,8 +20,9 @@ class BillingApi {
20
20
  constructor(ctx) {
21
21
  this.ctx = ctx;
22
22
  }
23
- async renewProductSubscription(request) {
24
- return (0, client_1.parseContract)(client_1.personalPlanCheckoutResultCheck, await this.ctx.request('POST', '/billing/product-subscriptions/renew', (0, client_1.parseContract)(client_1.productSubscriptionRenewalRequestCheck, request), { cache: false }));
23
+ /** The payment provider's customer portal for one named source the caller pays for. */
24
+ async openProductSubscriptionPortal(request) {
25
+ return (0, client_1.parseContract)(client_1.productSubscriptionPortalResponseCheck, await this.ctx.request('POST', '/billing/product-subscriptions/portal', (0, client_1.parseContract)(client_1.productSubscriptionPortalRequestCheck, request), { cache: false }));
25
26
  }
26
27
  async startProductPlanCheckout(request) {
27
28
  return (0, client_1.parseContract)(client_1.personalPlanCheckoutResultCheck, await this.ctx.request('POST', '/billing/checkout/product-plan', (0, client_1.parseContract)(client_1.productPlanCheckoutRequestCheck, request), { cache: false }));
@@ -49,6 +49,7 @@ const OxyServices_1 = require("../OxyServices");
49
49
  const HttpService_1 = require("../HttpService");
50
50
  const authenticatedFetch_1 = require("../authenticatedFetch");
51
51
  const requestSignal_1 = require("../utils/requestSignal");
52
+ const officialOrigins_1 = require("../utils/officialOrigins");
52
53
  const errorUtils_1 = require("../utils/errorUtils");
53
54
  const logger_1 = require("../logger");
54
55
  const workloadIdentity_1 = require("./workloadIdentity");
@@ -193,6 +194,34 @@ class OxyServer extends OxyServices_1.OxyServices {
193
194
  applicationAccessFor: (req, applicationId, options) => this.applicationAccessFor(req, applicationId, options),
194
195
  });
195
196
  }
197
+ /**
198
+ * Whether `url` names Oxy's own API — the first-party service this server is
199
+ * configured for (`baseURL`), when that origin is an official Oxy origin.
200
+ *
201
+ * For a URL a caller chose (a customer webhook endpoint, say) that may name
202
+ * Oxy itself: `true` sends it through this server
203
+ * (`createServiceClient({ baseURL: oxy.baseURL, audience: 'oxy-api' }).fetch`),
204
+ * `false` through `safeFetch`. Inside the VPC `api.oxy.so` resolves to a
205
+ * private address, which `safeFetch` refuses by design.
206
+ *
207
+ * The comparison is by exact origin (scheme, host and port): a look-alike
208
+ * (`api.oxy.so.evil.com`), another official host, a credential-carrying URL
209
+ * or anything unparseable is `false`, and so goes on to `safeFetch`'s
210
+ * checks. Never decide first-party by hostname suffix or branding.
211
+ */
212
+ isFirstPartyServiceUrl(url) {
213
+ let target;
214
+ try {
215
+ target = new URL(String(url));
216
+ }
217
+ catch {
218
+ return false;
219
+ }
220
+ if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password)
221
+ return false;
222
+ const own = new URL(this.baseURL).origin;
223
+ return target.origin === own && (0, officialOrigins_1.isOfficialWebOrigin)(own);
224
+ }
196
225
  /** A product SDK transport authenticated only by this process's workload. */
197
226
  createServiceClient(options) {
198
227
  const origin = new URL(options.baseURL);
@@ -9,7 +9,7 @@
9
9
  * `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
10
10
  * arrive as ISO-8601 strings.
11
11
  */
12
- import { cancelProductSubscriptionCheck, parseContract, personalPlanCatalogueCheck, personalPlanCheckoutRequestCheck, personalPlanCheckoutResultCheck, productPlanCatalogueCheck, productPlanCheckoutRequestCheck, productSubscriptionCancellationResultCheck, productSubscriptionRenewalRequestCheck, productSubscriptionsResponseCheck, subjectProductAccessCheck, subjectProductAccessQueryCheck, subscriptionCreditGrantsResponseCheck, } from '@oxy.so/contracts/client';
12
+ import { cancelProductSubscriptionCheck, parseContract, personalPlanCatalogueCheck, personalPlanCheckoutRequestCheck, personalPlanCheckoutResultCheck, productPlanCatalogueCheck, productPlanCheckoutRequestCheck, productSubscriptionCancellationResultCheck, productSubscriptionPortalRequestCheck, productSubscriptionPortalResponseCheck, productSubscriptionsResponseCheck, subjectProductAccessCheck, subjectProductAccessQueryCheck, subscriptionCreditGrantsResponseCheck, } from '@oxy.so/contracts/client';
13
13
  const enc = encodeURIComponent;
14
14
  const SUBSCRIPTION_TTL = 2 * 60 * 1000;
15
15
  const WALLET_TTL = 60 * 1000;
@@ -17,8 +17,9 @@ export class BillingApi {
17
17
  constructor(ctx) {
18
18
  this.ctx = ctx;
19
19
  }
20
- async renewProductSubscription(request) {
21
- return parseContract(personalPlanCheckoutResultCheck, await this.ctx.request('POST', '/billing/product-subscriptions/renew', parseContract(productSubscriptionRenewalRequestCheck, request), { cache: false }));
20
+ /** The payment provider's customer portal for one named source the caller pays for. */
21
+ async openProductSubscriptionPortal(request) {
22
+ return parseContract(productSubscriptionPortalResponseCheck, await this.ctx.request('POST', '/billing/product-subscriptions/portal', parseContract(productSubscriptionPortalRequestCheck, request), { cache: false }));
22
23
  }
23
24
  async startProductPlanCheckout(request) {
24
25
  return parseContract(personalPlanCheckoutResultCheck, await this.ctx.request('POST', '/billing/checkout/product-plan', parseContract(productPlanCheckoutRequestCheck, request), { cache: false }));
@@ -46,6 +46,7 @@ import { OxyServices } from '../OxyServices.js';
46
46
  import { HttpService } from '../HttpService.js';
47
47
  import { createAuthenticatedFetch } from '../authenticatedFetch.js';
48
48
  import { awaitRequestSignal } from '../utils/requestSignal.js';
49
+ import { isOfficialWebOrigin } from '../utils/officialOrigins.js';
49
50
  import { createCancelledError, extractErrorStatus } from '../utils/errorUtils.js';
50
51
  import { logger } from '../logger/index.js';
51
52
  import { canAttestWorkloadIdentity, requestWorkloadServiceToken, } from './workloadIdentity.js';
@@ -189,6 +190,34 @@ export class OxyServer extends OxyServices {
189
190
  applicationAccessFor: (req, applicationId, options) => this.applicationAccessFor(req, applicationId, options),
190
191
  });
191
192
  }
193
+ /**
194
+ * Whether `url` names Oxy's own API — the first-party service this server is
195
+ * configured for (`baseURL`), when that origin is an official Oxy origin.
196
+ *
197
+ * For a URL a caller chose (a customer webhook endpoint, say) that may name
198
+ * Oxy itself: `true` sends it through this server
199
+ * (`createServiceClient({ baseURL: oxy.baseURL, audience: 'oxy-api' }).fetch`),
200
+ * `false` through `safeFetch`. Inside the VPC `api.oxy.so` resolves to a
201
+ * private address, which `safeFetch` refuses by design.
202
+ *
203
+ * The comparison is by exact origin (scheme, host and port): a look-alike
204
+ * (`api.oxy.so.evil.com`), another official host, a credential-carrying URL
205
+ * or anything unparseable is `false`, and so goes on to `safeFetch`'s
206
+ * checks. Never decide first-party by hostname suffix or branding.
207
+ */
208
+ isFirstPartyServiceUrl(url) {
209
+ let target;
210
+ try {
211
+ target = new URL(String(url));
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password)
217
+ return false;
218
+ const own = new URL(this.baseURL).origin;
219
+ return target.origin === own && isOfficialWebOrigin(own);
220
+ }
192
221
  /** A product SDK transport authenticated only by this process's workload. */
193
222
  createServiceClient(options) {
194
223
  const origin = new URL(options.baseURL);
@@ -9,7 +9,7 @@
9
9
  * `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
10
10
  * arrive as ISO-8601 strings.
11
11
  */
12
- import type { PersonalPlanCatalogue, PersonalPlanCheckoutRequest, PersonalPlanCheckoutResult, ProductPlanCatalogue, ProductPlanCheckoutRequest, ProductSubscriptionCancellationResult, ProductSubscriptionRenewalRequest, ProductSubscriptionSummary, SubjectProductAccess, SubjectProductAccessQuery, SubscriptionCreditGrant } from '@oxy.so/contracts';
12
+ import type { PersonalPlanCatalogue, PersonalPlanCheckoutRequest, PersonalPlanCheckoutResult, ProductPlanCatalogue, ProductPlanCheckoutRequest, ProductSubscriptionCancellationResult, ProductSubscriptionPortalRequest, ProductSubscriptionPortalResponse, ProductSubscriptionSummary, SubjectProductAccess, SubjectProductAccessQuery, SubscriptionCreditGrant } from '@oxy.so/contracts';
13
13
  import type { OxyContext } from '../client/context';
14
14
  /** Subscription tier. */
15
15
  export type SubscriptionPlan = 'basic' | 'pro' | 'business';
@@ -98,7 +98,8 @@ export interface WalletTransactionsPage {
98
98
  export declare class BillingApi {
99
99
  protected readonly ctx: OxyContext;
100
100
  constructor(ctx: OxyContext);
101
- renewProductSubscription(request: ProductSubscriptionRenewalRequest): Promise<PersonalPlanCheckoutResult>;
101
+ /** The payment provider's customer portal for one named source the caller pays for. */
102
+ openProductSubscriptionPortal(request: ProductSubscriptionPortalRequest): Promise<ProductSubscriptionPortalResponse>;
102
103
  startProductPlanCheckout(request: ProductPlanCheckoutRequest): Promise<PersonalPlanCheckoutResult>;
103
104
  productPlans(productId: string): Promise<ProductPlanCatalogue>;
104
105
  startPersonalPlanCheckout(request: PersonalPlanCheckoutRequest): Promise<PersonalPlanCheckoutResult>;
@@ -196,6 +196,22 @@ export declare class OxyServer extends OxyServices {
196
196
  */
197
197
  readonly middleware: OxyMiddleware;
198
198
  constructor(config: OxyServerConfig);
199
+ /**
200
+ * Whether `url` names Oxy's own API — the first-party service this server is
201
+ * configured for (`baseURL`), when that origin is an official Oxy origin.
202
+ *
203
+ * For a URL a caller chose (a customer webhook endpoint, say) that may name
204
+ * Oxy itself: `true` sends it through this server
205
+ * (`createServiceClient({ baseURL: oxy.baseURL, audience: 'oxy-api' }).fetch`),
206
+ * `false` through `safeFetch`. Inside the VPC `api.oxy.so` resolves to a
207
+ * private address, which `safeFetch` refuses by design.
208
+ *
209
+ * The comparison is by exact origin (scheme, host and port): a look-alike
210
+ * (`api.oxy.so.evil.com`), another official host, a credential-carrying URL
211
+ * or anything unparseable is `false`, and so goes on to `safeFetch`'s
212
+ * checks. Never decide first-party by hostname suffix or branding.
213
+ */
214
+ isFirstPartyServiceUrl(url: string | URL): boolean;
199
215
  /** A product SDK transport authenticated only by this process's workload. */
200
216
  createServiceClient(options: ServiceClientOptions): ServiceHttpClient;
201
217
  /** Live service authority; human sessions and keys have a separate resource contract. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "7.21.1",
3
+ "version": "7.23.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -181,7 +181,7 @@
181
181
  "dependencies": {
182
182
  "@noble/ciphers": "^2.4.0",
183
183
  "@noble/hashes": "^2.4.0",
184
- "@oxy.so/contracts": "6.8.0",
184
+ "@oxy.so/contracts": "6.9.0",
185
185
  "@oxy.so/protocol": "^1.6.1",
186
186
  "@oxy.so/telemetry": "^2.1.0",
187
187
  "@scure/bip39": "^2.4.0",
@@ -17,7 +17,8 @@ import type {
17
17
  ProductPlanCatalogue,
18
18
  ProductPlanCheckoutRequest,
19
19
  ProductSubscriptionCancellationResult,
20
- ProductSubscriptionRenewalRequest,
20
+ ProductSubscriptionPortalRequest,
21
+ ProductSubscriptionPortalResponse,
21
22
  ProductSubscriptionSummary,
22
23
  SubjectProductAccess,
23
24
  SubjectProductAccessQuery,
@@ -32,7 +33,8 @@ import {
32
33
  productPlanCatalogueCheck,
33
34
  productPlanCheckoutRequestCheck,
34
35
  productSubscriptionCancellationResultCheck,
35
- productSubscriptionRenewalRequestCheck,
36
+ productSubscriptionPortalRequestCheck,
37
+ productSubscriptionPortalResponseCheck,
36
38
  productSubscriptionsResponseCheck,
37
39
  subjectProductAccessCheck,
38
40
  subjectProductAccessQueryCheck,
@@ -136,15 +138,16 @@ export interface WalletTransactionsPage {
136
138
  export class BillingApi {
137
139
  constructor(protected readonly ctx: OxyContext) {}
138
140
 
139
- async renewProductSubscription(
140
- request: ProductSubscriptionRenewalRequest,
141
- ): Promise<PersonalPlanCheckoutResult> {
141
+ /** The payment provider's customer portal for one named source the caller pays for. */
142
+ async openProductSubscriptionPortal(
143
+ request: ProductSubscriptionPortalRequest,
144
+ ): Promise<ProductSubscriptionPortalResponse> {
142
145
  return parseContract(
143
- personalPlanCheckoutResultCheck,
146
+ productSubscriptionPortalResponseCheck,
144
147
  await this.ctx.request<unknown>(
145
148
  'POST',
146
- '/billing/product-subscriptions/renew',
147
- parseContract(productSubscriptionRenewalRequestCheck, request),
149
+ '/billing/product-subscriptions/portal',
150
+ parseContract(productSubscriptionPortalRequestCheck, request),
148
151
  { cache: false },
149
152
  ),
150
153
  );
@@ -66,6 +66,7 @@ import { OxyServices, type OxyConfig } from '../OxyServices';
66
66
  import { HttpService } from '../HttpService';
67
67
  import { createAuthenticatedFetch } from '../authenticatedFetch';
68
68
  import { awaitRequestSignal } from '../utils/requestSignal';
69
+ import { isOfficialWebOrigin } from '../utils/officialOrigins';
69
70
  import { createCancelledError, extractErrorStatus } from '../utils/errorUtils';
70
71
  import type { HttpMethod, ServiceLane } from '../client/context';
71
72
  import type { RequestOptions, ResponsePayload } from '../HttpService';
@@ -410,6 +411,34 @@ export class OxyServer extends OxyServices {
410
411
  });
411
412
  }
412
413
 
414
+ /**
415
+ * Whether `url` names Oxy's own API — the first-party service this server is
416
+ * configured for (`baseURL`), when that origin is an official Oxy origin.
417
+ *
418
+ * For a URL a caller chose (a customer webhook endpoint, say) that may name
419
+ * Oxy itself: `true` sends it through this server
420
+ * (`createServiceClient({ baseURL: oxy.baseURL, audience: 'oxy-api' }).fetch`),
421
+ * `false` through `safeFetch`. Inside the VPC `api.oxy.so` resolves to a
422
+ * private address, which `safeFetch` refuses by design.
423
+ *
424
+ * The comparison is by exact origin (scheme, host and port): a look-alike
425
+ * (`api.oxy.so.evil.com`), another official host, a credential-carrying URL
426
+ * or anything unparseable is `false`, and so goes on to `safeFetch`'s
427
+ * checks. Never decide first-party by hostname suffix or branding.
428
+ */
429
+ isFirstPartyServiceUrl(url: string | URL): boolean {
430
+ let target: URL;
431
+ try {
432
+ target = new URL(String(url));
433
+ } catch {
434
+ return false;
435
+ }
436
+ if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password)
437
+ return false;
438
+ const own = new URL(this.baseURL).origin;
439
+ return target.origin === own && isOfficialWebOrigin(own);
440
+ }
441
+
413
442
  /** A product SDK transport authenticated only by this process's workload. */
414
443
  createServiceClient(options: ServiceClientOptions): ServiceHttpClient {
415
444
  const origin = new URL(options.baseURL);