@oxy.so/core 4.3.0 → 4.5.0-oxy-one.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.
@@ -51,6 +51,24 @@ class AssetsApi {
51
51
  constructor(ctx) {
52
52
  this.ctx = ctx;
53
53
  }
54
+ /** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
55
+ async putPresignedUpload(init, body) {
56
+ if (!init.uploadUrl)
57
+ return; // bytes already exist for this account
58
+ const target = new URL(init.uploadUrl);
59
+ if (target.protocol !== 'https:')
60
+ throw new Error('Presigned uploads require HTTPS');
61
+ const headers = new Headers(init.requiredHeaders);
62
+ headers.forEach((_value, name) => {
63
+ if (!['if-none-match', 'content-type', 'x-amz-checksum-sha256', 'cache-control'].includes(name.toLowerCase()))
64
+ throw new Error('Unsupported required upload header');
65
+ });
66
+ if (body.type && !headers.has('Content-Type'))
67
+ headers.set('Content-Type', body.type);
68
+ const result = await fetch(target.toString(), { method: 'PUT', body, headers, credentials: 'omit', redirect: 'error' });
69
+ if (!result.ok)
70
+ throw new Error(`Presigned upload failed (${result.status})`);
71
+ }
54
72
  // ── Upload ───────────────────────────────────────────────────────────────
55
73
  /**
56
74
  * Upload a file. Accepts a web `File`/`Blob` or a React Native
@@ -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 = contracts_1.subjectProductAccessQuerySchema.parse(query);
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 contracts_1.subjectProductAccessSchema.parse(response);
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 contracts_2.productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
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 contracts_2.subscriptionCreditGrantsResponseSchema.parse(await this.ctx.request("GET", "/billing/credit-grants", undefined, { cache: false })).grants;
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", contracts_2.cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
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 contracts_2.productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', contracts_2.cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
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() {
@@ -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
  *
@@ -162,6 +163,12 @@ class OxyServer extends OxyServices_1.OxyServices {
162
163
  this._serverAgency = new namespaces_1.ServerAgencyApi(this.context);
163
164
  return this._serverAgency;
164
165
  }
166
+ /** Application management, plus the receiver-bound Alia machine-key lookup. */
167
+ get apps() {
168
+ if (!this._serverApps)
169
+ this._serverApps = new namespaces_1.ServerAppsApi(this.context);
170
+ return this._serverApps;
171
+ }
165
172
  /** Reputation reads, plus `award`. */
166
173
  get reputation() {
167
174
  if (!this._serverReputation)
@@ -273,6 +280,26 @@ class OxyServer extends OxyServices_1.OxyServices {
273
280
  * request is on behalf of that user (the API checks the delegation grant).
274
281
  * Never cached unless `cache` says so. Rejects with `OxyApiError`.
275
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
+ }
276
303
  async serviceRequest(method, url, data, options = {}) {
277
304
  const { actAs, headers, ...rest } = options;
278
305
  const token = await this.serviceToken();
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OXY_ALIA_MACHINE_SCOPES = exports.OXY_ALIA_RESOURCE_APPLICATION_ID = void 0;
4
+ exports.isOxyAliaMachinePrincipal = isOxyAliaMachinePrincipal;
5
+ exports.getOxyAliaMachineCredentialBearer = getOxyAliaMachineCredentialBearer;
6
+ exports.createOxyAliaMachineCredentialAuth = createOxyAliaMachineCredentialAuth;
7
+ /** Stable resource identity, shared with the Oxy control plane. Not a display name. */
8
+ exports.OXY_ALIA_RESOURCE_APPLICATION_ID = '6a2f851751b784a86fd0e934';
9
+ exports.OXY_ALIA_MACHINE_SCOPES = ['alia:chat', 'inference:invoke'];
10
+ function id(value) {
11
+ return typeof value === 'string' && value.length > 0 && value.length <= 128 && value.trim() === value;
12
+ }
13
+ /** Strict response boundary: extra user/tier fields cannot smuggle authority. */
14
+ function isOxyAliaMachinePrincipal(value) {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value))
16
+ return false;
17
+ const p = value;
18
+ const keys = ['kind', 'audience', 'applicationId', 'credentialId', 'ownerAccountId', 'environment', 'scopes'];
19
+ return Object.keys(p).length === keys.length && keys.every(key => Object.prototype.hasOwnProperty.call(p, key))
20
+ && p.kind === 'machine' && p.audience === exports.OXY_ALIA_RESOURCE_APPLICATION_ID
21
+ && id(p.applicationId) && id(p.credentialId) && id(p.ownerAccountId)
22
+ && typeof p.environment === 'string' && ['production', 'staging', 'development'].includes(p.environment)
23
+ && Array.isArray(p.scopes) && p.scopes.length === 2
24
+ && exports.OXY_ALIA_MACHINE_SCOPES.every(scope => p.scopes.includes(scope));
25
+ }
26
+ // Keep the forwarding credential out of enumerable request/principal objects,
27
+ // logs, response serialization and persistent SDK/session state. No cache.
28
+ const forwardingCredentials = new WeakMap();
29
+ /** Only for the request-scoped Oxy inference client; never for a user API. */
30
+ function getOxyAliaMachineCredentialBearer(req) {
31
+ const bound = forwardingCredentials.get(req);
32
+ return bound && req.machineCredential === bound.principal ? bound.bearer : null;
33
+ }
34
+ /** Opt into Alia's app-only chat lane on its two chat routes, never globally. */
35
+ function createOxyAliaMachineCredentialAuth(server, options) {
36
+ return async (req, res, next) => {
37
+ const auth = req.headers.authorization;
38
+ const token = typeof auth === 'string' && auth.startsWith('Bearer ') ? auth.slice(7) : '';
39
+ if (!token.startsWith('oxy_sk_') || token.length > 2048) {
40
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
41
+ return;
42
+ }
43
+ const existing = req;
44
+ if (existing.user || existing.userId || existing.serviceApp || existing.oxyRequester
45
+ || req.machineCredential
46
+ || req.headers['x-oxy-user-id'] !== undefined || req.headers['x-oxy-requester-assertion'] !== undefined) {
47
+ res.status(403).json({ error: 'MACHINE_USER_DELEGATION_UNSUPPORTED' });
48
+ return;
49
+ }
50
+ try {
51
+ const answer = await server.apps.introspectAliaMachineCredential(token);
52
+ if (!answer.active || !isOxyAliaMachinePrincipal(answer.principal) || answer.principal.environment !== options.environment) {
53
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
54
+ return;
55
+ }
56
+ const principal = Object.freeze({ ...answer.principal, scopes: Object.freeze(['alia:chat', 'inference:invoke']) });
57
+ req.machineCredential = principal;
58
+ forwardingCredentials.set(req, { principal, bearer: token });
59
+ next();
60
+ }
61
+ catch {
62
+ // Transport errors may contain request data; never expose or log them.
63
+ res.status(503).json({ error: 'MACHINE_CREDENTIAL_VERIFIER_UNAVAILABLE' });
64
+ }
65
+ };
66
+ }
@@ -16,8 +16,8 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.getOxyActor = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
- exports.signInAgentAccount = exports.hasBoundedServiceTokenLifetime = exports.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS = exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.createLiveCapabilityTicketVerifier = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = void 0;
19
+ exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.getOxyActor = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.OXY_ALIA_MACHINE_SCOPES = exports.OXY_ALIA_RESOURCE_APPLICATION_ID = exports.isOxyAliaMachinePrincipal = exports.getOxyAliaMachineCredentialBearer = exports.createOxyAliaMachineCredentialAuth = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ServerAppsApi = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
+ exports.signInAgentAccount = exports.hasBoundedServiceTokenLifetime = exports.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS = exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.createLiveCapabilityTicketVerifier = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = void 0;
21
21
  // The backend client: OxyServices plus the service-token lane, middleware,
22
22
  // account events and the service-only namespace methods.
23
23
  var OxyServer_1 = require("./OxyServer");
@@ -25,11 +25,18 @@ Object.defineProperty(exports, "OxyServer", { enumerable: true, get: function ()
25
25
  Object.defineProperty(exports, "ServiceCredentialMismatchError", { enumerable: true, get: function () { return OxyServer_1.ServiceCredentialMismatchError; } });
26
26
  Object.defineProperty(exports, "ANONYMOUS_SERVICE_TOKEN_RETRY_MS", { enumerable: true, get: function () { return OxyServer_1.ANONYMOUS_SERVICE_TOKEN_RETRY_MS; } });
27
27
  var namespaces_1 = require("./namespaces");
28
+ Object.defineProperty(exports, "ServerAppsApi", { enumerable: true, get: function () { return namespaces_1.ServerAppsApi; } });
28
29
  Object.defineProperty(exports, "ServerAgencyApi", { enumerable: true, get: function () { return namespaces_1.ServerAgencyApi; } });
29
30
  Object.defineProperty(exports, "ServerAssetsApi", { enumerable: true, get: function () { return namespaces_1.ServerAssetsApi; } });
30
31
  Object.defineProperty(exports, "ServerLinkedAccountsApi", { enumerable: true, get: function () { return namespaces_1.ServerLinkedAccountsApi; } });
31
32
  Object.defineProperty(exports, "ServerNotificationsApi", { enumerable: true, get: function () { return namespaces_1.ServerNotificationsApi; } });
32
33
  Object.defineProperty(exports, "ServerReputationApi", { enumerable: true, get: function () { return namespaces_1.ServerReputationApi; } });
34
+ var aliaMachineCredential_1 = require("./aliaMachineCredential");
35
+ Object.defineProperty(exports, "createOxyAliaMachineCredentialAuth", { enumerable: true, get: function () { return aliaMachineCredential_1.createOxyAliaMachineCredentialAuth; } });
36
+ Object.defineProperty(exports, "getOxyAliaMachineCredentialBearer", { enumerable: true, get: function () { return aliaMachineCredential_1.getOxyAliaMachineCredentialBearer; } });
37
+ Object.defineProperty(exports, "isOxyAliaMachinePrincipal", { enumerable: true, get: function () { return aliaMachineCredential_1.isOxyAliaMachinePrincipal; } });
38
+ Object.defineProperty(exports, "OXY_ALIA_RESOURCE_APPLICATION_ID", { enumerable: true, get: function () { return aliaMachineCredential_1.OXY_ALIA_RESOURCE_APPLICATION_ID; } });
39
+ Object.defineProperty(exports, "OXY_ALIA_MACHINE_SCOPES", { enumerable: true, get: function () { return aliaMachineCredential_1.OXY_ALIA_MACHINE_SCOPES; } });
33
40
  var middleware_1 = require("./middleware");
34
41
  Object.defineProperty(exports, "OxyAccountEventError", { enumerable: true, get: function () { return middleware_1.OxyAccountEventError; } });
35
42
  Object.defineProperty(exports, "OXY_ACCOUNT_DELETED_EVENT_URI", { enumerable: true, get: function () { return middleware_1.OXY_ACCOUNT_DELETED_EVENT_URI; } });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServerReputationApi = exports.ServerAgencyApi = exports.ServerLinkedAccountsApi = exports.ServerNotificationsApi = exports.ServerAssetsApi = void 0;
3
+ exports.ServerReputationApi = exports.ServerAgencyApi = exports.ServerLinkedAccountsApi = exports.ServerNotificationsApi = exports.ServerAssetsApi = exports.ServerAppsApi = void 0;
4
4
  /**
5
5
  * The server-side halves of the namespaces: what exists only with a service
6
6
  * credential. `OxyServer` returns these from its namespace getters, so
@@ -13,6 +13,8 @@ const notifications_1 = require("../api/notifications");
13
13
  const linkedAccounts_1 = require("../api/linkedAccounts");
14
14
  const agency_1 = require("../api/agency");
15
15
  const reputation_1 = require("../api/reputation");
16
+ const apps_1 = require("../api/apps");
17
+ const aliaMachineCredential_1 = require("./aliaMachineCredential");
16
18
  const OxyServices_errors_1 = require("../OxyServices.errors");
17
19
  const errorUtils_1 = require("../utils/errorUtils");
18
20
  const logger_1 = require("../logger");
@@ -30,6 +32,28 @@ function lane(ctx) {
30
32
  }
31
33
  return ctx.service;
32
34
  }
35
+ class ServerAppsApi extends apps_1.AppsApi {
36
+ constructor(serverCtx) {
37
+ super(serverCtx);
38
+ this.serverCtx = serverCtx;
39
+ }
40
+ /** Live receiver-bound lookup using THIS resource server's own service token. */
41
+ async introspectAliaMachineCredential(token) {
42
+ if (!token.startsWith('oxy_sk_') || token.length > 2048)
43
+ return { active: false };
44
+ const answer = await lane(this.serverCtx).request('POST', '/internal/alia/machine-credentials/introspect', { token }, { cache: false, retry: false });
45
+ if (!answer || typeof answer !== 'object' || Array.isArray(answer))
46
+ throw new Error('Invalid machine credential verdict');
47
+ const record = answer;
48
+ if (record.active === false && Object.keys(record).length === 1)
49
+ return { active: false };
50
+ if (record.active !== true || Object.keys(record).length !== 2 || !(0, aliaMachineCredential_1.isOxyAliaMachinePrincipal)(record.principal)) {
51
+ throw new Error('Invalid machine credential verdict');
52
+ }
53
+ return { active: true, principal: record.principal };
54
+ }
55
+ }
56
+ exports.ServerAppsApi = ServerAppsApi;
33
57
  function chunk(items, size) {
34
58
  const chunks = [];
35
59
  for (let i = 0; i < items.length; i += size)
@@ -47,6 +47,24 @@ export class AssetsApi {
47
47
  constructor(ctx) {
48
48
  this.ctx = ctx;
49
49
  }
50
+ /** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
51
+ async putPresignedUpload(init, body) {
52
+ if (!init.uploadUrl)
53
+ return; // bytes already exist for this account
54
+ const target = new URL(init.uploadUrl);
55
+ if (target.protocol !== 'https:')
56
+ throw new Error('Presigned uploads require HTTPS');
57
+ const headers = new Headers(init.requiredHeaders);
58
+ headers.forEach((_value, name) => {
59
+ if (!['if-none-match', 'content-type', 'x-amz-checksum-sha256', 'cache-control'].includes(name.toLowerCase()))
60
+ throw new Error('Unsupported required upload header');
61
+ });
62
+ if (body.type && !headers.has('Content-Type'))
63
+ headers.set('Content-Type', body.type);
64
+ const result = await fetch(target.toString(), { method: 'PUT', body, headers, credentials: 'omit', redirect: 'error' });
65
+ if (!result.ok)
66
+ throw new Error(`Presigned upload failed (${result.status})`);
67
+ }
50
68
  // ── Upload ───────────────────────────────────────────────────────────────
51
69
  /**
52
70
  * Upload a file. Accepts a web `File`/`Blob` or a React Native
@@ -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() {
@@ -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
  *
@@ -43,7 +44,7 @@ import { OxyServices } from '../OxyServices.js';
43
44
  import { logger } from '../logger/index.js';
44
45
  import { canAttestWorkloadIdentity, requestWorkloadServiceToken } from './workloadIdentity.js';
45
46
  import { createOxyMiddleware, OXY_ACCOUNT_DELETED_EVENT_URI, OXY_JWT_ISSUER, OxyAccountEventError, parseJsonSegment, resolveServiceTokenPublicKey, } from './middleware.js';
46
- import { ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces.js';
47
+ import { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces.js';
47
48
  /**
48
49
  * How long a failed service-token mint sends session-less requests anonymous
49
50
  * before the next attempt, under `serviceIdentity: 'when-anonymous'`.
@@ -158,6 +159,12 @@ export class OxyServer extends OxyServices {
158
159
  this._serverAgency = new ServerAgencyApi(this.context);
159
160
  return this._serverAgency;
160
161
  }
162
+ /** Application management, plus the receiver-bound Alia machine-key lookup. */
163
+ get apps() {
164
+ if (!this._serverApps)
165
+ this._serverApps = new ServerAppsApi(this.context);
166
+ return this._serverApps;
167
+ }
161
168
  /** Reputation reads, plus `award`. */
162
169
  get reputation() {
163
170
  if (!this._serverReputation)
@@ -269,6 +276,26 @@ export class OxyServer extends OxyServices {
269
276
  * request is on behalf of that user (the API checks the delegation grant).
270
277
  * Never cached unless `cache` says so. Rejects with `OxyApiError`.
271
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
+ }
272
299
  async serviceRequest(method, url, data, options = {}) {
273
300
  const { actAs, headers, ...rest } = options;
274
301
  const token = await this.serviceToken();
@@ -0,0 +1,60 @@
1
+ /** Stable resource identity, shared with the Oxy control plane. Not a display name. */
2
+ export const OXY_ALIA_RESOURCE_APPLICATION_ID = '6a2f851751b784a86fd0e934';
3
+ export const OXY_ALIA_MACHINE_SCOPES = ['alia:chat', 'inference:invoke'];
4
+ function id(value) {
5
+ return typeof value === 'string' && value.length > 0 && value.length <= 128 && value.trim() === value;
6
+ }
7
+ /** Strict response boundary: extra user/tier fields cannot smuggle authority. */
8
+ export function isOxyAliaMachinePrincipal(value) {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value))
10
+ return false;
11
+ const p = value;
12
+ const keys = ['kind', 'audience', 'applicationId', 'credentialId', 'ownerAccountId', 'environment', 'scopes'];
13
+ return Object.keys(p).length === keys.length && keys.every(key => Object.prototype.hasOwnProperty.call(p, key))
14
+ && p.kind === 'machine' && p.audience === OXY_ALIA_RESOURCE_APPLICATION_ID
15
+ && id(p.applicationId) && id(p.credentialId) && id(p.ownerAccountId)
16
+ && typeof p.environment === 'string' && ['production', 'staging', 'development'].includes(p.environment)
17
+ && Array.isArray(p.scopes) && p.scopes.length === 2
18
+ && OXY_ALIA_MACHINE_SCOPES.every(scope => p.scopes.includes(scope));
19
+ }
20
+ // Keep the forwarding credential out of enumerable request/principal objects,
21
+ // logs, response serialization and persistent SDK/session state. No cache.
22
+ const forwardingCredentials = new WeakMap();
23
+ /** Only for the request-scoped Oxy inference client; never for a user API. */
24
+ export function getOxyAliaMachineCredentialBearer(req) {
25
+ const bound = forwardingCredentials.get(req);
26
+ return bound && req.machineCredential === bound.principal ? bound.bearer : null;
27
+ }
28
+ /** Opt into Alia's app-only chat lane on its two chat routes, never globally. */
29
+ export function createOxyAliaMachineCredentialAuth(server, options) {
30
+ return async (req, res, next) => {
31
+ const auth = req.headers.authorization;
32
+ const token = typeof auth === 'string' && auth.startsWith('Bearer ') ? auth.slice(7) : '';
33
+ if (!token.startsWith('oxy_sk_') || token.length > 2048) {
34
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
35
+ return;
36
+ }
37
+ const existing = req;
38
+ if (existing.user || existing.userId || existing.serviceApp || existing.oxyRequester
39
+ || req.machineCredential
40
+ || req.headers['x-oxy-user-id'] !== undefined || req.headers['x-oxy-requester-assertion'] !== undefined) {
41
+ res.status(403).json({ error: 'MACHINE_USER_DELEGATION_UNSUPPORTED' });
42
+ return;
43
+ }
44
+ try {
45
+ const answer = await server.apps.introspectAliaMachineCredential(token);
46
+ if (!answer.active || !isOxyAliaMachinePrincipal(answer.principal) || answer.principal.environment !== options.environment) {
47
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
48
+ return;
49
+ }
50
+ const principal = Object.freeze({ ...answer.principal, scopes: Object.freeze(['alia:chat', 'inference:invoke']) });
51
+ req.machineCredential = principal;
52
+ forwardingCredentials.set(req, { principal, bearer: token });
53
+ next();
54
+ }
55
+ catch {
56
+ // Transport errors may contain request data; never expose or log them.
57
+ res.status(503).json({ error: 'MACHINE_CREDENTIAL_VERIFIER_UNAVAILABLE' });
58
+ }
59
+ };
60
+ }
@@ -17,7 +17,8 @@
17
17
  // The backend client: OxyServices plus the service-token lane, middleware,
18
18
  // account events and the service-only namespace methods.
19
19
  export { OxyServer, ServiceCredentialMismatchError, ANONYMOUS_SERVICE_TOKEN_RETRY_MS } from './OxyServer.js';
20
- export { ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces.js';
20
+ export { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces.js';
21
+ export { createOxyAliaMachineCredentialAuth, getOxyAliaMachineCredentialBearer, isOxyAliaMachinePrincipal, OXY_ALIA_RESOURCE_APPLICATION_ID, OXY_ALIA_MACHINE_SCOPES, } from './aliaMachineCredential.js';
21
22
  export { OxyAccountEventError, OXY_ACCOUNT_DELETED_EVENT_URI } from './middleware.js';
22
23
  export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyActor, getOxyAuthRefusal, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth.js';
23
24
  export { createOxyRateLimit } from './rateLimit.js';
@@ -10,6 +10,8 @@ import { NotificationsApi } from '../api/notifications.js';
10
10
  import { LinkedAccountsApi } from '../api/linkedAccounts.js';
11
11
  import { AgencyApi } from '../api/agency.js';
12
12
  import { ReputationApi } from '../api/reputation.js';
13
+ import { AppsApi } from '../api/apps.js';
14
+ import { isOxyAliaMachinePrincipal } from './aliaMachineCredential.js';
13
15
  import { ServiceAssetMetadataError, ServiceLinkedDownloadUrlError } from '../OxyServices.errors.js';
14
16
  import { extractErrorStatus } from '../utils/errorUtils.js';
15
17
  import { logger } from '../logger/index.js';
@@ -27,6 +29,27 @@ function lane(ctx) {
27
29
  }
28
30
  return ctx.service;
29
31
  }
32
+ export class ServerAppsApi extends AppsApi {
33
+ constructor(serverCtx) {
34
+ super(serverCtx);
35
+ this.serverCtx = serverCtx;
36
+ }
37
+ /** Live receiver-bound lookup using THIS resource server's own service token. */
38
+ async introspectAliaMachineCredential(token) {
39
+ if (!token.startsWith('oxy_sk_') || token.length > 2048)
40
+ return { active: false };
41
+ const answer = await lane(this.serverCtx).request('POST', '/internal/alia/machine-credentials/introspect', { token }, { cache: false, retry: false });
42
+ if (!answer || typeof answer !== 'object' || Array.isArray(answer))
43
+ throw new Error('Invalid machine credential verdict');
44
+ const record = answer;
45
+ if (record.active === false && Object.keys(record).length === 1)
46
+ return { active: false };
47
+ if (record.active !== true || Object.keys(record).length !== 2 || !isOxyAliaMachinePrincipal(record.principal)) {
48
+ throw new Error('Invalid machine credential verdict');
49
+ }
50
+ return { active: true, principal: record.principal };
51
+ }
52
+ }
30
53
  function chunk(items, size) {
31
54
  const chunks = [];
32
55
  for (let i = 0; i < items.length; i += size)
@@ -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.
@@ -41,6 +41,8 @@ export interface AssetDeleteResult {
41
41
  export declare class AssetsApi {
42
42
  protected readonly ctx: OxyContext;
43
43
  constructor(ctx: OxyContext);
44
+ /** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
45
+ putPresignedUpload(init: AssetInitResponse, body: Blob): Promise<void>;
44
46
  /**
45
47
  * Upload a file. Accepts a web `File`/`Blob` or a React Native
46
48
  * `{ uri, type?, name?, size? }` descriptor.
@@ -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(): Promise<User>;
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,8 +1,9 @@
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';
4
5
  import { type OxyAccountEvent, type OxyAccountEventFeedPage, type OxyMiddleware, type ServiceActingAsVerification, type VerifyAccountEventOptions } from './middleware';
5
- import { ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi } from './namespaces';
6
+ import { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi } from './namespaces';
6
7
  export interface OxyServerConfig extends OxyConfig {
7
8
  /** This service's application credential. Omit to use workload identity (ADR 0026). */
8
9
  serviceAuth?: {
@@ -87,6 +88,7 @@ export declare class OxyServer extends OxyServices {
87
88
  private _serverNotifications?;
88
89
  private _serverLinkedAccounts?;
89
90
  private _serverAgency?;
91
+ private _serverApps?;
90
92
  private _serverReputation?;
91
93
  /** Files, plus the service-only metadata and linked-URL lookups. */
92
94
  get assets(): ServerAssetsApi;
@@ -96,6 +98,8 @@ export declare class OxyServer extends OxyServices {
96
98
  get linkedAccounts(): ServerLinkedAccountsApi;
97
99
  /** Delegated capabilities, plus `introspectRequesterAssertion`. */
98
100
  get agency(): ServerAgencyApi;
101
+ /** Application management, plus the receiver-bound Alia machine-key lookup. */
102
+ get apps(): ServerAppsApi;
99
103
  /** Reputation reads, plus `award`. */
100
104
  get reputation(): ServerReputationApi;
101
105
  /**
@@ -129,6 +133,10 @@ export declare class OxyServer extends OxyServices {
129
133
  * request is on behalf of that user (the API checks the delegation grant).
130
134
  * Never cached unless `cache` says so. Rejects with `OxyApiError`.
131
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>;
132
140
  serviceRequest<T>(method: HttpMethod, url: string, data?: unknown, options?: RequestOptions & {
133
141
  actAs?: string;
134
142
  }): Promise<T>;
@@ -0,0 +1,37 @@
1
+ import type { Request, RequestHandler } from 'express';
2
+ import type { OxyServiceEnvironment } from './auth';
3
+ /** Stable resource identity, shared with the Oxy control plane. Not a display name. */
4
+ export declare const OXY_ALIA_RESOURCE_APPLICATION_ID = "6a2f851751b784a86fd0e934";
5
+ export declare const OXY_ALIA_MACHINE_SCOPES: readonly ["alia:chat", "inference:invoke"];
6
+ /** App-only assistant turn: no human subject, service tier or delegated authority. */
7
+ export interface OxyAliaMachinePrincipal {
8
+ readonly kind: 'machine';
9
+ readonly audience: typeof OXY_ALIA_RESOURCE_APPLICATION_ID;
10
+ readonly applicationId: string;
11
+ readonly credentialId: string;
12
+ readonly ownerAccountId: string;
13
+ readonly environment: OxyServiceEnvironment;
14
+ readonly scopes: readonly ['alia:chat', 'inference:invoke'];
15
+ }
16
+ export type AliaMachineCredentialIntrospection = {
17
+ active: false;
18
+ } | {
19
+ active: true;
20
+ principal: OxyAliaMachinePrincipal;
21
+ };
22
+ export interface OxyAliaMachineCredentialHost {
23
+ readonly apps: {
24
+ introspectAliaMachineCredential(token: string): Promise<AliaMachineCredentialIntrospection>;
25
+ };
26
+ }
27
+ export interface OxyAliaMachineRequest extends Request {
28
+ machineCredential?: OxyAliaMachinePrincipal;
29
+ }
30
+ /** Strict response boundary: extra user/tier fields cannot smuggle authority. */
31
+ export declare function isOxyAliaMachinePrincipal(value: unknown): value is OxyAliaMachinePrincipal;
32
+ /** Only for the request-scoped Oxy inference client; never for a user API. */
33
+ export declare function getOxyAliaMachineCredentialBearer(req: Request): string | null;
34
+ /** Opt into Alia's app-only chat lane on its two chat routes, never globally. */
35
+ export declare function createOxyAliaMachineCredentialAuth(server: OxyAliaMachineCredentialHost, options: {
36
+ environment: OxyServiceEnvironment;
37
+ }): RequestHandler;
@@ -16,7 +16,9 @@
16
16
  */
17
17
  export { OxyServer, ServiceCredentialMismatchError, ANONYMOUS_SERVICE_TOKEN_RETRY_MS } from './OxyServer';
18
18
  export type { OxyServerConfig, ServiceTokenResponse } from './OxyServer';
19
- export { ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces';
19
+ export { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces';
20
+ export { createOxyAliaMachineCredentialAuth, getOxyAliaMachineCredentialBearer, isOxyAliaMachinePrincipal, OXY_ALIA_RESOURCE_APPLICATION_ID, OXY_ALIA_MACHINE_SCOPES, } from './aliaMachineCredential';
21
+ export type { AliaMachineCredentialIntrospection, OxyAliaMachinePrincipal, OxyAliaMachineRequest, OxyAliaMachineCredentialHost, } from './aliaMachineCredential';
20
22
  export type { CapabilityTicketGrant, CapabilityTicketIntrospection, CreateExecutionAuthorizationInput, ServiceCapabilityCatalog, } from './namespaces';
21
23
  export { OxyAccountEventError, OXY_ACCOUNT_DELETED_EVENT_URI } from './middleware';
22
24
  export type { AuthMiddlewareOptions, OxyAccountEvent, OxyAccountEventFeedItem, OxyAccountEventFeedPage, OxyMiddleware, ServiceActingAsVerification, ServiceApp, VerifyAccountEventOptions, } from './middleware';
@@ -5,12 +5,21 @@
5
5
  * `oxy.assets`.
6
6
  */
7
7
  import { type ForegroundExecutionAuthorizationInput, type ActorRef, type AppCapabilityCatalog, type AutonomyLevel, type CapabilityTicketClaims, type CapabilityTicketRequest, type GrantLimit, type PolicyDecision, type ResourceRef, type CreateOxyNotificationRequest, type AwardReputationInput, type ReputationTransaction, type ServiceLinkedAccountListResponse } from '@oxy.so/contracts';
8
+ import type { OxyContext } from '../client/context';
8
9
  import type { Notification, ServiceAssetMetadata, ServiceAssetMetadataBySha, ServiceLinkedDownloadUrl } from '../models/interfaces';
9
10
  import { AssetsApi } from '../api/assets';
10
11
  import { NotificationsApi } from '../api/notifications';
11
12
  import { LinkedAccountsApi } from '../api/linkedAccounts';
12
13
  import { AgencyApi, type CapabilityExecutionAuthorization, type RequesterAssertionGrant, type RequesterAssertionIntrospection } from '../api/agency';
13
14
  import { ReputationApi } from '../api/reputation';
15
+ import { AppsApi } from '../api/apps';
16
+ import { type AliaMachineCredentialIntrospection } from './aliaMachineCredential';
17
+ export declare class ServerAppsApi extends AppsApi {
18
+ private readonly serverCtx;
19
+ constructor(serverCtx: OxyContext);
20
+ /** Live receiver-bound lookup using THIS resource server's own service token. */
21
+ introspectAliaMachineCredential(token: string): Promise<AliaMachineCredentialIntrospection>;
22
+ }
14
23
  export declare class ServerAssetsApi extends AssetsApi {
15
24
  /**
16
25
  * Resolve many asset ids to their content-addressed metadata (`sha256`,
@@ -36,8 +36,8 @@ export declare const oxyRequesterAssertionClaimsSchema: z.ZodObject<{
36
36
  /** The one native agent this requester may reach with it. */
37
37
  agentId: z.ZodString;
38
38
  }, "strict", z.ZodTypeAny, {
39
- exp: number;
40
39
  sub: string;
40
+ exp: number;
41
41
  aud: string;
42
42
  iss: string;
43
43
  jti: string;
@@ -46,8 +46,8 @@ export declare const oxyRequesterAssertionClaimsSchema: z.ZodObject<{
46
46
  cid: string;
47
47
  agentId: string;
48
48
  }, {
49
- exp: number;
50
49
  sub: string;
50
+ exp: number;
51
51
  aud: string;
52
52
  iss: string;
53
53
  jti: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "4.3.0",
3
+ "version": "4.5.0-oxy-one.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",
@@ -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": "^4.9.1",
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,
@@ -101,6 +102,21 @@ export interface AssetDeleteResult {
101
102
  export class AssetsApi {
102
103
  constructor(protected readonly ctx: OxyContext) {}
103
104
 
105
+ /** Upload already-admitted bytes. Sends required bucket headers, never an Oxy bearer. */
106
+ async putPresignedUpload(init: AssetInitResponse, body: Blob): Promise<void> {
107
+ if (!init.uploadUrl) return; // bytes already exist for this account
108
+ const target = new URL(init.uploadUrl);
109
+ if (target.protocol !== 'https:') throw new Error('Presigned uploads require HTTPS');
110
+ const headers = new Headers(init.requiredHeaders);
111
+ headers.forEach((_value,name) => {
112
+ if (!['if-none-match','content-type','x-amz-checksum-sha256','cache-control'].includes(name.toLowerCase()))
113
+ throw new Error('Unsupported required upload header');
114
+ });
115
+ if (body.type && !headers.has('Content-Type')) headers.set('Content-Type',body.type);
116
+ const result = await fetch(target.toString(),{method:'PUT',body,headers,credentials:'omit',redirect:'error'});
117
+ if(!result.ok)throw new Error(`Presigned upload failed (${result.status})`);
118
+ }
119
+
104
120
  // ── Upload ───────────────────────────────────────────────────────────────
105
121
 
106
122
  /**
@@ -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
 
@@ -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;
@@ -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
  *
@@ -59,6 +60,7 @@ import {
59
60
  type VerifyAccountEventOptions,
60
61
  } from './middleware';
61
62
  import {
63
+ ServerAppsApi,
62
64
  ServerAgencyApi,
63
65
  ServerAssetsApi,
64
66
  ServerLinkedAccountsApi,
@@ -217,6 +219,7 @@ export class OxyServer extends OxyServices {
217
219
  private _serverNotifications?: ServerNotificationsApi;
218
220
  private _serverLinkedAccounts?: ServerLinkedAccountsApi;
219
221
  private _serverAgency?: ServerAgencyApi;
222
+ private _serverApps?: ServerAppsApi;
220
223
  private _serverReputation?: ServerReputationApi;
221
224
 
222
225
  /** Files, plus the service-only metadata and linked-URL lookups. */
@@ -239,6 +242,11 @@ export class OxyServer extends OxyServices {
239
242
  if (!this._serverAgency) this._serverAgency = new ServerAgencyApi(this.context);
240
243
  return this._serverAgency;
241
244
  }
245
+ /** Application management, plus the receiver-bound Alia machine-key lookup. */
246
+ override get apps(): ServerAppsApi {
247
+ if (!this._serverApps) this._serverApps = new ServerAppsApi(this.context);
248
+ return this._serverApps;
249
+ }
242
250
  /** Reputation reads, plus `award`. */
243
251
  override get reputation(): ServerReputationApi {
244
252
  if (!this._serverReputation) this._serverReputation = new ServerReputationApi(this.context);
@@ -355,6 +363,31 @@ export class OxyServer extends OxyServices {
355
363
  * request is on behalf of that user (the API checks the delegation grant).
356
364
  * Never cached unless `cache` says so. Rejects with `OxyApiError`.
357
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
+
358
391
  async serviceRequest<T>(
359
392
  method: HttpMethod,
360
393
  url: string,
@@ -695,4 +728,3 @@ export class OxyServer extends OxyServices {
695
728
  return appId;
696
729
  }
697
730
  }
698
-
@@ -0,0 +1,94 @@
1
+ import type { Request, RequestHandler } from 'express';
2
+ import type { OxyServiceEnvironment } from './auth';
3
+
4
+ /** Stable resource identity, shared with the Oxy control plane. Not a display name. */
5
+ export const OXY_ALIA_RESOURCE_APPLICATION_ID = '6a2f851751b784a86fd0e934';
6
+ export const OXY_ALIA_MACHINE_SCOPES = ['alia:chat', 'inference:invoke'] as const;
7
+
8
+ /** App-only assistant turn: no human subject, service tier or delegated authority. */
9
+ export interface OxyAliaMachinePrincipal {
10
+ readonly kind: 'machine';
11
+ readonly audience: typeof OXY_ALIA_RESOURCE_APPLICATION_ID;
12
+ readonly applicationId: string;
13
+ readonly credentialId: string;
14
+ readonly ownerAccountId: string;
15
+ readonly environment: OxyServiceEnvironment;
16
+ readonly scopes: readonly ['alia:chat', 'inference:invoke'];
17
+ }
18
+
19
+ export type AliaMachineCredentialIntrospection =
20
+ | { active: false }
21
+ | { active: true; principal: OxyAliaMachinePrincipal };
22
+
23
+ export interface OxyAliaMachineCredentialHost {
24
+ readonly apps: {
25
+ introspectAliaMachineCredential(token: string): Promise<AliaMachineCredentialIntrospection>;
26
+ };
27
+ }
28
+
29
+ export interface OxyAliaMachineRequest extends Request {
30
+ machineCredential?: OxyAliaMachinePrincipal;
31
+ }
32
+
33
+ function id(value: unknown): value is string {
34
+ return typeof value === 'string' && value.length > 0 && value.length <= 128 && value.trim() === value;
35
+ }
36
+
37
+ /** Strict response boundary: extra user/tier fields cannot smuggle authority. */
38
+ export function isOxyAliaMachinePrincipal(value: unknown): value is OxyAliaMachinePrincipal {
39
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
40
+ const p = value as Record<string, unknown>;
41
+ const keys = ['kind', 'audience', 'applicationId', 'credentialId', 'ownerAccountId', 'environment', 'scopes'];
42
+ return Object.keys(p).length === keys.length && keys.every(key => Object.prototype.hasOwnProperty.call(p, key))
43
+ && p.kind === 'machine' && p.audience === OXY_ALIA_RESOURCE_APPLICATION_ID
44
+ && id(p.applicationId) && id(p.credentialId) && id(p.ownerAccountId)
45
+ && typeof p.environment === 'string' && ['production', 'staging', 'development'].includes(p.environment)
46
+ && Array.isArray(p.scopes) && p.scopes.length === 2
47
+ && OXY_ALIA_MACHINE_SCOPES.every(scope => (p.scopes as unknown[]).includes(scope));
48
+ }
49
+
50
+ // Keep the forwarding credential out of enumerable request/principal objects,
51
+ // logs, response serialization and persistent SDK/session state. No cache.
52
+ const forwardingCredentials = new WeakMap<Request, { principal: OxyAliaMachinePrincipal; bearer: string }>();
53
+
54
+ /** Only for the request-scoped Oxy inference client; never for a user API. */
55
+ export function getOxyAliaMachineCredentialBearer(req: Request): string | null {
56
+ const bound = forwardingCredentials.get(req);
57
+ return bound && (req as OxyAliaMachineRequest).machineCredential === bound.principal ? bound.bearer : null;
58
+ }
59
+
60
+ /** Opt into Alia's app-only chat lane on its two chat routes, never globally. */
61
+ export function createOxyAliaMachineCredentialAuth(
62
+ server: OxyAliaMachineCredentialHost,
63
+ options: { environment: OxyServiceEnvironment },
64
+ ): RequestHandler {
65
+ return async (req, res, next) => {
66
+ const auth = req.headers.authorization;
67
+ const token = typeof auth === 'string' && auth.startsWith('Bearer ') ? auth.slice(7) : '';
68
+ if (!token.startsWith('oxy_sk_') || token.length > 2048) {
69
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
70
+ return;
71
+ }
72
+ const existing = req as Request & { user?: unknown; userId?: unknown; serviceApp?: unknown; oxyRequester?: unknown };
73
+ if (existing.user || existing.userId || existing.serviceApp || existing.oxyRequester
74
+ || (req as OxyAliaMachineRequest).machineCredential
75
+ || req.headers['x-oxy-user-id'] !== undefined || req.headers['x-oxy-requester-assertion'] !== undefined) {
76
+ res.status(403).json({ error: 'MACHINE_USER_DELEGATION_UNSUPPORTED' });
77
+ return;
78
+ }
79
+ try {
80
+ const answer = await server.apps.introspectAliaMachineCredential(token);
81
+ if (!answer.active || !isOxyAliaMachinePrincipal(answer.principal) || answer.principal.environment !== options.environment) {
82
+ res.status(401).json({ error: 'MACHINE_CREDENTIAL_INVALID' });
83
+ return;
84
+ }
85
+ const principal = Object.freeze({ ...answer.principal, scopes: Object.freeze(['alia:chat', 'inference:invoke'] as const) });
86
+ (req as OxyAliaMachineRequest).machineCredential = principal;
87
+ forwardingCredentials.set(req, { principal, bearer: token });
88
+ next();
89
+ } catch {
90
+ // Transport errors may contain request data; never expose or log them.
91
+ res.status(503).json({ error: 'MACHINE_CREDENTIAL_VERIFIER_UNAVAILABLE' });
92
+ }
93
+ };
94
+ }
@@ -20,12 +20,26 @@
20
20
  export { OxyServer, ServiceCredentialMismatchError, ANONYMOUS_SERVICE_TOKEN_RETRY_MS } from './OxyServer';
21
21
  export type { OxyServerConfig, ServiceTokenResponse } from './OxyServer';
22
22
  export {
23
+ ServerAppsApi,
23
24
  ServerAgencyApi,
24
25
  ServerAssetsApi,
25
26
  ServerLinkedAccountsApi,
26
27
  ServerNotificationsApi,
27
28
  ServerReputationApi,
28
29
  } from './namespaces';
30
+ export {
31
+ createOxyAliaMachineCredentialAuth,
32
+ getOxyAliaMachineCredentialBearer,
33
+ isOxyAliaMachinePrincipal,
34
+ OXY_ALIA_RESOURCE_APPLICATION_ID,
35
+ OXY_ALIA_MACHINE_SCOPES,
36
+ } from './aliaMachineCredential';
37
+ export type {
38
+ AliaMachineCredentialIntrospection,
39
+ OxyAliaMachinePrincipal,
40
+ OxyAliaMachineRequest,
41
+ OxyAliaMachineCredentialHost,
42
+ } from './aliaMachineCredential';
29
43
  export type {
30
44
  CapabilityTicketGrant,
31
45
  CapabilityTicketIntrospection,
@@ -18,6 +18,8 @@ import { NotificationsApi } from '../api/notifications';
18
18
  import { LinkedAccountsApi } from '../api/linkedAccounts';
19
19
  import { AgencyApi, type CapabilityExecutionAuthorization, type RequesterAssertionGrant, type RequesterAssertionIntrospection } from '../api/agency';
20
20
  import { ReputationApi } from '../api/reputation';
21
+ import { AppsApi } from '../api/apps';
22
+ import { isOxyAliaMachinePrincipal, type AliaMachineCredentialIntrospection } from './aliaMachineCredential';
21
23
  import { ServiceAssetMetadataError, ServiceLinkedDownloadUrlError } from '../OxyServices.errors';
22
24
  import { extractErrorStatus } from '../utils/errorUtils';
23
25
  import { logger } from '../logger';
@@ -39,6 +41,25 @@ function lane(ctx: OxyContext): ServiceLane {
39
41
  return ctx.service;
40
42
  }
41
43
 
44
+ export class ServerAppsApi extends AppsApi {
45
+ constructor(private readonly serverCtx: OxyContext) { super(serverCtx); }
46
+ /** Live receiver-bound lookup using THIS resource server's own service token. */
47
+ async introspectAliaMachineCredential(token: string): Promise<AliaMachineCredentialIntrospection> {
48
+ if (!token.startsWith('oxy_sk_') || token.length > 2048) return { active: false };
49
+ const answer = await lane(this.serverCtx).request<unknown>(
50
+ 'POST', '/internal/alia/machine-credentials/introspect', { token },
51
+ { cache: false, retry: false },
52
+ );
53
+ if (!answer || typeof answer !== 'object' || Array.isArray(answer)) throw new Error('Invalid machine credential verdict');
54
+ const record = answer as Record<string, unknown>;
55
+ if (record.active === false && Object.keys(record).length === 1) return { active: false };
56
+ if (record.active !== true || Object.keys(record).length !== 2 || !isOxyAliaMachinePrincipal(record.principal)) {
57
+ throw new Error('Invalid machine credential verdict');
58
+ }
59
+ return { active: true, principal: record.principal };
60
+ }
61
+ }
62
+
42
63
  function chunk<T>(items: T[], size: number): T[][] {
43
64
  const chunks: T[][] = [];
44
65
  for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));