@oxy.so/core 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/inference/OxyInferenceClient.js +11 -0
- package/dist/cjs/server/OxyServer.js +6 -0
- package/dist/cjs/server/aliaMachineCredential.js +66 -0
- package/dist/cjs/server/index.js +9 -2
- package/dist/cjs/server/namespaces.js +25 -1
- package/dist/esm/inference/OxyInferenceClient.js +11 -0
- package/dist/esm/server/OxyServer.js +7 -1
- package/dist/esm/server/aliaMachineCredential.js +60 -0
- package/dist/esm/server/index.js +2 -1
- package/dist/esm/server/namespaces.js +23 -0
- package/dist/types/inference/OxyInferenceClient.d.ts +9 -0
- package/dist/types/server/OxyServer.d.ts +4 -1
- package/dist/types/server/aliaMachineCredential.d.ts +37 -0
- package/dist/types/server/index.d.ts +3 -1
- package/dist/types/server/namespaces.d.ts +9 -0
- package/package.json +2 -2
- package/src/inference/OxyInferenceClient.ts +16 -0
- package/src/server/OxyServer.ts +7 -1
- package/src/server/aliaMachineCredential.ts +94 -0
- package/src/server/index.ts +14 -0
- package/src/server/namespaces.ts +21 -0
|
@@ -395,6 +395,17 @@ class OxyInferenceClient {
|
|
|
395
395
|
const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, options);
|
|
396
396
|
return body.data;
|
|
397
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Recover the existing record using the original credential and key. This
|
|
400
|
+
* performs only GET; 404 is unknown/pending, never proof of zero cost and
|
|
401
|
+
* never an instruction to repeat decide(). Lost answers are not retained.
|
|
402
|
+
*/
|
|
403
|
+
async getGenerationRecordByIdempotencyKey(key, options = {}) {
|
|
404
|
+
if (key.length === 0 || key.length > 128)
|
|
405
|
+
throw new Error('Invalid original idempotency key.');
|
|
406
|
+
const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', '/v1/generations/by-idempotency-key', { ...options, idempotencyKey: key });
|
|
407
|
+
return body.data;
|
|
408
|
+
}
|
|
398
409
|
}
|
|
399
410
|
exports.OxyInferenceClient = OxyInferenceClient;
|
|
400
411
|
_OxyInferenceClient_baseURL = new WeakMap(), _OxyInferenceClient_credential = new WeakMap(), _OxyInferenceClient_fetch = new WeakMap(), _OxyInferenceClient_instances = new WeakSet(), _OxyInferenceClient_bearer =
|
|
@@ -162,6 +162,12 @@ class OxyServer extends OxyServices_1.OxyServices {
|
|
|
162
162
|
this._serverAgency = new namespaces_1.ServerAgencyApi(this.context);
|
|
163
163
|
return this._serverAgency;
|
|
164
164
|
}
|
|
165
|
+
/** Application management, plus the receiver-bound Alia machine-key lookup. */
|
|
166
|
+
get apps() {
|
|
167
|
+
if (!this._serverApps)
|
|
168
|
+
this._serverApps = new namespaces_1.ServerAppsApi(this.context);
|
|
169
|
+
return this._serverApps;
|
|
170
|
+
}
|
|
165
171
|
/** Reputation reads, plus `award`. */
|
|
166
172
|
get reputation() {
|
|
167
173
|
if (!this._serverReputation)
|
|
@@ -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
|
+
}
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.
|
|
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)
|
|
@@ -390,6 +390,17 @@ export class OxyInferenceClient {
|
|
|
390
390
|
const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, options);
|
|
391
391
|
return body.data;
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Recover the existing record using the original credential and key. This
|
|
395
|
+
* performs only GET; 404 is unknown/pending, never proof of zero cost and
|
|
396
|
+
* never an instruction to repeat decide(). Lost answers are not retained.
|
|
397
|
+
*/
|
|
398
|
+
async getGenerationRecordByIdempotencyKey(key, options = {}) {
|
|
399
|
+
if (key.length === 0 || key.length > 128)
|
|
400
|
+
throw new Error('Invalid original idempotency key.');
|
|
401
|
+
const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', '/v1/generations/by-idempotency-key', { ...options, idempotencyKey: key });
|
|
402
|
+
return body.data;
|
|
403
|
+
}
|
|
393
404
|
}
|
|
394
405
|
_OxyInferenceClient_baseURL = new WeakMap(), _OxyInferenceClient_credential = new WeakMap(), _OxyInferenceClient_fetch = new WeakMap(), _OxyInferenceClient_instances = new WeakSet(), _OxyInferenceClient_bearer =
|
|
395
406
|
/** The bearer for this request, from whichever lane was configured. */
|
|
@@ -43,7 +43,7 @@ import { OxyServices } from '../OxyServices.js';
|
|
|
43
43
|
import { logger } from '../logger/index.js';
|
|
44
44
|
import { canAttestWorkloadIdentity, requestWorkloadServiceToken } from './workloadIdentity.js';
|
|
45
45
|
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';
|
|
46
|
+
import { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces.js';
|
|
47
47
|
/**
|
|
48
48
|
* How long a failed service-token mint sends session-less requests anonymous
|
|
49
49
|
* before the next attempt, under `serviceIdentity: 'when-anonymous'`.
|
|
@@ -158,6 +158,12 @@ export class OxyServer extends OxyServices {
|
|
|
158
158
|
this._serverAgency = new ServerAgencyApi(this.context);
|
|
159
159
|
return this._serverAgency;
|
|
160
160
|
}
|
|
161
|
+
/** Application management, plus the receiver-bound Alia machine-key lookup. */
|
|
162
|
+
get apps() {
|
|
163
|
+
if (!this._serverApps)
|
|
164
|
+
this._serverApps = new ServerAppsApi(this.context);
|
|
165
|
+
return this._serverApps;
|
|
166
|
+
}
|
|
161
167
|
/** Reputation reads, plus `award`. */
|
|
162
168
|
get reputation() {
|
|
163
169
|
if (!this._serverReputation)
|
|
@@ -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
|
+
}
|
package/dist/esm/server/index.js
CHANGED
|
@@ -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)
|
|
@@ -382,4 +382,13 @@ export declare class OxyInferenceClient {
|
|
|
382
382
|
signal?: AbortSignal;
|
|
383
383
|
delegatedUserId?: string;
|
|
384
384
|
}): Promise<OxyGenerationRecord>;
|
|
385
|
+
/**
|
|
386
|
+
* Recover the existing record using the original credential and key. This
|
|
387
|
+
* performs only GET; 404 is unknown/pending, never proof of zero cost and
|
|
388
|
+
* never an instruction to repeat decide(). Lost answers are not retained.
|
|
389
|
+
*/
|
|
390
|
+
getGenerationRecordByIdempotencyKey(key: string, options?: {
|
|
391
|
+
signal?: AbortSignal;
|
|
392
|
+
delegatedUserId?: string;
|
|
393
|
+
}): Promise<OxyGenerationRecord>;
|
|
385
394
|
}
|
|
@@ -2,7 +2,7 @@ import { OxyServices, type OxyConfig } from '../OxyServices';
|
|
|
2
2
|
import type { HttpMethod } from '../client/context';
|
|
3
3
|
import type { RequestOptions } from '../HttpService';
|
|
4
4
|
import { type OxyAccountEvent, type OxyAccountEventFeedPage, type OxyMiddleware, type ServiceActingAsVerification, type VerifyAccountEventOptions } from './middleware';
|
|
5
|
-
import { ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi } from './namespaces';
|
|
5
|
+
import { ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi } from './namespaces';
|
|
6
6
|
export interface OxyServerConfig extends OxyConfig {
|
|
7
7
|
/** This service's application credential. Omit to use workload identity (ADR 0026). */
|
|
8
8
|
serviceAuth?: {
|
|
@@ -87,6 +87,7 @@ export declare class OxyServer extends OxyServices {
|
|
|
87
87
|
private _serverNotifications?;
|
|
88
88
|
private _serverLinkedAccounts?;
|
|
89
89
|
private _serverAgency?;
|
|
90
|
+
private _serverApps?;
|
|
90
91
|
private _serverReputation?;
|
|
91
92
|
/** Files, plus the service-only metadata and linked-URL lookups. */
|
|
92
93
|
get assets(): ServerAssetsApi;
|
|
@@ -96,6 +97,8 @@ export declare class OxyServer extends OxyServices {
|
|
|
96
97
|
get linkedAccounts(): ServerLinkedAccountsApi;
|
|
97
98
|
/** Delegated capabilities, plus `introspectRequesterAssertion`. */
|
|
98
99
|
get agency(): ServerAgencyApi;
|
|
100
|
+
/** Application management, plus the receiver-bound Alia machine-key lookup. */
|
|
101
|
+
get apps(): ServerAppsApi;
|
|
99
102
|
/** Reputation reads, plus `award`. */
|
|
100
103
|
get reputation(): ServerReputationApi;
|
|
101
104
|
/**
|
|
@@ -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`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.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.
|
|
182
|
+
"@oxy.so/contracts": "^4.9.1",
|
|
183
183
|
"@oxy.so/protocol": "^1.2.2",
|
|
184
184
|
"@scure/bip39": "^1.6.0",
|
|
185
185
|
"buffer": "^6.0.3",
|
|
@@ -712,6 +712,22 @@ export class OxyInferenceClient {
|
|
|
712
712
|
return body.data;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Recover the existing record using the original credential and key. This
|
|
717
|
+
* performs only GET; 404 is unknown/pending, never proof of zero cost and
|
|
718
|
+
* never an instruction to repeat decide(). Lost answers are not retained.
|
|
719
|
+
*/
|
|
720
|
+
async getGenerationRecordByIdempotencyKey(
|
|
721
|
+
key: string,
|
|
722
|
+
options: { signal?: AbortSignal; delegatedUserId?: string } = {},
|
|
723
|
+
): Promise<OxyGenerationRecord> {
|
|
724
|
+
if (key.length === 0 || key.length > 128) throw new Error('Invalid original idempotency key.');
|
|
725
|
+
const body = await this.#request<{ data: OxyGenerationRecord }>(
|
|
726
|
+
'GET', '/v1/generations/by-idempotency-key', { ...options, idempotencyKey: key },
|
|
727
|
+
);
|
|
728
|
+
return body.data;
|
|
729
|
+
}
|
|
730
|
+
|
|
715
731
|
/** The bearer for this request, from whichever lane was configured. */
|
|
716
732
|
async #bearer(): Promise<string> {
|
|
717
733
|
const value =
|
package/src/server/OxyServer.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
type VerifyAccountEventOptions,
|
|
60
60
|
} from './middleware';
|
|
61
61
|
import {
|
|
62
|
+
ServerAppsApi,
|
|
62
63
|
ServerAgencyApi,
|
|
63
64
|
ServerAssetsApi,
|
|
64
65
|
ServerLinkedAccountsApi,
|
|
@@ -217,6 +218,7 @@ export class OxyServer extends OxyServices {
|
|
|
217
218
|
private _serverNotifications?: ServerNotificationsApi;
|
|
218
219
|
private _serverLinkedAccounts?: ServerLinkedAccountsApi;
|
|
219
220
|
private _serverAgency?: ServerAgencyApi;
|
|
221
|
+
private _serverApps?: ServerAppsApi;
|
|
220
222
|
private _serverReputation?: ServerReputationApi;
|
|
221
223
|
|
|
222
224
|
/** Files, plus the service-only metadata and linked-URL lookups. */
|
|
@@ -239,6 +241,11 @@ export class OxyServer extends OxyServices {
|
|
|
239
241
|
if (!this._serverAgency) this._serverAgency = new ServerAgencyApi(this.context);
|
|
240
242
|
return this._serverAgency;
|
|
241
243
|
}
|
|
244
|
+
/** Application management, plus the receiver-bound Alia machine-key lookup. */
|
|
245
|
+
override get apps(): ServerAppsApi {
|
|
246
|
+
if (!this._serverApps) this._serverApps = new ServerAppsApi(this.context);
|
|
247
|
+
return this._serverApps;
|
|
248
|
+
}
|
|
242
249
|
/** Reputation reads, plus `award`. */
|
|
243
250
|
override get reputation(): ServerReputationApi {
|
|
244
251
|
if (!this._serverReputation) this._serverReputation = new ServerReputationApi(this.context);
|
|
@@ -695,4 +702,3 @@ export class OxyServer extends OxyServices {
|
|
|
695
702
|
return appId;
|
|
696
703
|
}
|
|
697
704
|
}
|
|
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
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -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,
|
package/src/server/namespaces.ts
CHANGED
|
@@ -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));
|