@oxy.so/core 1.3.1 → 1.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/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.agency.js +41 -0
- package/dist/cjs/server/index.js +16 -1
- package/dist/cjs/server/requesterAssertion.js +362 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.agency.js +41 -0
- package/dist/esm/server/index.js +3 -0
- package/dist/esm/server/requesterAssertion.js +353 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.agency.d.ts +41 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/requesterAssertion.d.ts +151 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/mixins/OxyServices.agency.ts +76 -0
- package/src/server/__tests__/requesterAssertion.test.ts +359 -0
- package/src/server/index.ts +27 -0
- package/src/server/requesterAssertion.ts +465 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
|
|
|
39
39
|
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
40
40
|
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
41
41
|
export type { PublicApplication, ConnectedApp, ConnectedMcpClient, } from './mixins/OxyServices.connectedApps';
|
|
42
|
-
export type { AccountCapabilityPolicy, AvailableCapabilityCatalog, CapabilityExecutionAuthorization, CreateDelegationGrantInput, DelegationCatalogBinding, DelegationGrantView, PutAccountCapabilityPolicyInput, UpdateDelegationGrantInput, } from './mixins/OxyServices.agency';
|
|
42
|
+
export type { AccountCapabilityPolicy, AvailableCapabilityCatalog, CapabilityExecutionAuthorization, CreateDelegationGrantInput, DelegationCatalogBinding, DelegationGrantView, PutAccountCapabilityPolicyInput, RequesterAssertionGrant, RequesterAssertionIntrospection, UpdateDelegationGrantInput, } from './mixins/OxyServices.agency';
|
|
43
43
|
export type { StoreCategory, StoreRating, StoreListingSummary, StoreListingDetail, StoreScreenshot, StoreScreenshotPlatform, StoreReview, StoreOwnReview, WriteStoreReviewInput, StoreListingStatus, PublisherListing, WriteListingInput, AddScreenshotInput, UpdateScreenshotInput, StorePage, StorePageOptions, StoreReviewsOptions, } from './mixins/OxyServices.store';
|
|
44
44
|
export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, RotateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
45
45
|
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts';
|
|
@@ -69,6 +69,23 @@ export interface CapabilityExecutionAuthorization {
|
|
|
69
69
|
createdAt: string;
|
|
70
70
|
updatedAt: string;
|
|
71
71
|
}
|
|
72
|
+
/** A present-requester assertion minted for a product backend (ADR 0025). */
|
|
73
|
+
export interface RequesterAssertionGrant {
|
|
74
|
+
assertion: string;
|
|
75
|
+
expiresAt: string;
|
|
76
|
+
requesterAccountId: string;
|
|
77
|
+
agentId: string;
|
|
78
|
+
}
|
|
79
|
+
/** Oxy's live answer to the audience that consumed an assertion (ADR 0025). */
|
|
80
|
+
export interface RequesterAssertionIntrospection {
|
|
81
|
+
active: boolean;
|
|
82
|
+
requesterAccountId?: string;
|
|
83
|
+
agentId?: string;
|
|
84
|
+
applicationId?: string;
|
|
85
|
+
credentialId?: string;
|
|
86
|
+
jti?: string;
|
|
87
|
+
expiresAt?: string;
|
|
88
|
+
}
|
|
72
89
|
export declare function OxyServicesAgencyMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
73
90
|
new (...args: any[]): {
|
|
74
91
|
listAvailableCapabilityCatalogs(accountId: string): Promise<AvailableCapabilityCatalog[]>;
|
|
@@ -82,6 +99,30 @@ export declare function OxyServicesAgencyMixin<T extends typeof OxyServicesBase>
|
|
|
82
99
|
listCapabilityExecutionAuthorizations(ownerAccountId: string): Promise<CapabilityExecutionAuthorization[]>;
|
|
83
100
|
revokeCapabilityExecutionAuthorization(authorizationId: string, ownerAccountId: string): Promise<void>;
|
|
84
101
|
listCapabilityAuditEvents(accountId: string): Promise<AuditEvent[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Product backend → Oxy: trade the signed-in requester's access token for a
|
|
104
|
+
* one-use assertion naming `agentId` (ADR 0025). Authenticated with THIS
|
|
105
|
+
* instance's service credential (`configureServiceAuth`). `subjectToken` is
|
|
106
|
+
* sent to Oxy only, in the body; never forward it anywhere else.
|
|
107
|
+
*
|
|
108
|
+
* Not retried: a refusal is an answer, and the caller mints per turn.
|
|
109
|
+
*/
|
|
110
|
+
mintRequesterAssertion(input: {
|
|
111
|
+
agentId: string;
|
|
112
|
+
subjectToken: string;
|
|
113
|
+
}): Promise<RequesterAssertionGrant>;
|
|
114
|
+
/**
|
|
115
|
+
* Audience → Oxy: verify, live-revalidate and CONSUME an assertion. A second
|
|
116
|
+
* call with the same assertion answers `active: false`. Authenticated with
|
|
117
|
+
* the audience's own service credential.
|
|
118
|
+
*/
|
|
119
|
+
introspectRequesterAssertion(input: {
|
|
120
|
+
assertion: string;
|
|
121
|
+
presenter: {
|
|
122
|
+
applicationId: string;
|
|
123
|
+
credentialId: string;
|
|
124
|
+
};
|
|
125
|
+
}): Promise<RequesterAssertionIntrospection>;
|
|
85
126
|
httpService: import("../HttpService").HttpService;
|
|
86
127
|
cloudURL: string;
|
|
87
128
|
config: import("../OxyServices.base").OxyConfig;
|
|
@@ -26,6 +26,8 @@ export { buildOxyCspDirectives, buildOxyPagesHeaders, createOxySecurityHeaders,
|
|
|
26
26
|
export type { OxyCspDirective, OxyCspExtensions, OxyPagesHeadersOptions, OxySecurityHeadersOptions, } from './securityHeaders';
|
|
27
27
|
export { CapabilityTicketError, createCapabilityTicketMiddleware, inputSatisfiesCapabilityLimits, issueCapabilityTicket, readCapabilityAuthorization, verifyCapabilityTicket, } from './capabilityTicket';
|
|
28
28
|
export type { CapabilityTicketMiddlewareOptions, CapabilityTicketRequest, CapabilityTicketSigningOptions, CapabilityTicketVerificationOptions, } from './capabilityTicket';
|
|
29
|
+
export { createOxyJwksKeyResolver, createOxyRequesterAssertionAuth, OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER, OXY_REQUESTER_ASSERTION_HEADER, OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS, OXY_REQUESTER_ASSERTION_TYPE, OxyRequesterAssertionError, oxyRequesterAssertionClaimsSchema, readOxyRequesterAssertionKeyId, signOxyRequesterAssertion, verifyOxyRequesterAssertion, } from './requesterAssertion';
|
|
30
|
+
export type { OxyJwksKeyResolverOptions, OxyRequesterAssertionAuthOptions, OxyRequesterAssertionClaims, OxyRequesterAssertionErrorCode, OxyRequesterAssertionIntrospection, OxyRequesterAssertionIntrospector, OxyRequesterAssertionRequest, OxyRequesterAssertionVerificationOptions, OxyRequesterContext, } from './requesterAssertion';
|
|
29
31
|
export { verifySecret } from './verifySecret';
|
|
30
32
|
export { createOxyUserInvalidationHandler, publishOxyUserInvalidation, } from './userInvalidation';
|
|
31
33
|
export type { OxyInvalidationPublisher, OxyUserInvalidationHandlerOptions, } from './userInvalidation';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Present-requester assertions (ADR 0025).
|
|
3
|
+
*
|
|
4
|
+
* A first-party product backend holding a signed-in person's live Oxy session
|
|
5
|
+
* trades it with Oxy (`OxyServices.mintRequesterAssertion`) for a one-use,
|
|
6
|
+
* short-lived `OXY-REQUESTER+JWT`, and sends that — never the person's bearer —
|
|
7
|
+
* to the audience service (Alia) beside its own service token. The audience
|
|
8
|
+
* mounts {@link createOxyRequesterAssertionAuth} after `createOxyAuthMiddleware`
|
|
9
|
+
* and gets `req.userId` from the verified, introspected and consumed assertion.
|
|
10
|
+
*
|
|
11
|
+
* Signing and stateless verification live here so Oxy (the signer) and every
|
|
12
|
+
* verifier share one implementation of the format.
|
|
13
|
+
*/
|
|
14
|
+
import { type KeyObject } from 'node:crypto';
|
|
15
|
+
import type { RequestHandler } from 'express';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import type { OxyAuthRequest } from './auth';
|
|
18
|
+
export declare const OXY_REQUESTER_ASSERTION_TYPE = "OXY-REQUESTER+JWT";
|
|
19
|
+
/** The request header an audience reads the assertion from. */
|
|
20
|
+
export declare const OXY_REQUESTER_ASSERTION_HEADER = "x-oxy-requester-assertion";
|
|
21
|
+
/** Oxy signs for 120 s; no verifier accepts a lifetime above this ceiling. */
|
|
22
|
+
export declare const OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = 300;
|
|
23
|
+
export declare const OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = "https://api.oxy.so";
|
|
24
|
+
export declare const oxyRequesterAssertionClaimsSchema: z.ZodObject<{
|
|
25
|
+
iss: z.ZodString;
|
|
26
|
+
aud: z.ZodString;
|
|
27
|
+
/** The requester account. */
|
|
28
|
+
sub: z.ZodString;
|
|
29
|
+
jti: z.ZodString;
|
|
30
|
+
iat: z.ZodNumber;
|
|
31
|
+
exp: z.ZodNumber;
|
|
32
|
+
/** The application the assertion was minted for, and the only valid presenter. */
|
|
33
|
+
azp: z.ZodString;
|
|
34
|
+
/** The exact credential of that application. */
|
|
35
|
+
cid: z.ZodString;
|
|
36
|
+
/** The one native agent this requester may reach with it. */
|
|
37
|
+
agentId: z.ZodString;
|
|
38
|
+
}, "strict", z.ZodTypeAny, {
|
|
39
|
+
exp: number;
|
|
40
|
+
sub: string;
|
|
41
|
+
aud: string;
|
|
42
|
+
iss: string;
|
|
43
|
+
iat: number;
|
|
44
|
+
jti: string;
|
|
45
|
+
azp: string;
|
|
46
|
+
cid: string;
|
|
47
|
+
agentId: string;
|
|
48
|
+
}, {
|
|
49
|
+
exp: number;
|
|
50
|
+
sub: string;
|
|
51
|
+
aud: string;
|
|
52
|
+
iss: string;
|
|
53
|
+
iat: number;
|
|
54
|
+
jti: string;
|
|
55
|
+
azp: string;
|
|
56
|
+
cid: string;
|
|
57
|
+
agentId: string;
|
|
58
|
+
}>;
|
|
59
|
+
export type OxyRequesterAssertionClaims = z.infer<typeof oxyRequesterAssertionClaimsSchema>;
|
|
60
|
+
export type OxyRequesterAssertionErrorCode = 'malformed' | 'unknown_key' | 'invalid_signature' | 'invalid_claims' | 'wrong_issuer' | 'wrong_audience' | 'not_yet_valid' | 'expired' | 'ttl_exceeded';
|
|
61
|
+
export declare class OxyRequesterAssertionError extends Error {
|
|
62
|
+
readonly code: OxyRequesterAssertionErrorCode;
|
|
63
|
+
constructor(code: OxyRequesterAssertionErrorCode);
|
|
64
|
+
}
|
|
65
|
+
/** Signs an assertion. Oxy is the only intended caller. */
|
|
66
|
+
export declare function signOxyRequesterAssertion(claims: OxyRequesterAssertionClaims, signing: {
|
|
67
|
+
readonly keyId: string;
|
|
68
|
+
readonly privateKey: KeyObject;
|
|
69
|
+
}): string;
|
|
70
|
+
/** Reads the `kid` without trusting anything else in the token. */
|
|
71
|
+
export declare function readOxyRequesterAssertionKeyId(token: string): string;
|
|
72
|
+
export interface OxyRequesterAssertionVerificationOptions {
|
|
73
|
+
readonly publicKey: KeyObject | undefined;
|
|
74
|
+
readonly issuer: string;
|
|
75
|
+
readonly audience: string;
|
|
76
|
+
readonly now?: Date;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Stateless verification: header, signature, claim shape, issuer, audience and
|
|
80
|
+
* lifetime. It does NOT prove the assertion is unused or that the requester's
|
|
81
|
+
* session is still live — only introspection does.
|
|
82
|
+
*/
|
|
83
|
+
export declare function verifyOxyRequesterAssertion(token: string, options: OxyRequesterAssertionVerificationOptions): OxyRequesterAssertionClaims;
|
|
84
|
+
export interface OxyJwksKeyResolverOptions {
|
|
85
|
+
readonly jwksUrl: string;
|
|
86
|
+
readonly fetch?: typeof fetch;
|
|
87
|
+
readonly clock?: () => number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Cached Ed25519 JWKS resolver. An unknown `kid` refreshes at most once a
|
|
91
|
+
* minute, so a flood of forged key ids cannot turn into a flood of fetches. A
|
|
92
|
+
* failed refresh keeps serving keys that are still inside their cache window.
|
|
93
|
+
*/
|
|
94
|
+
export declare function createOxyJwksKeyResolver(options: OxyJwksKeyResolverOptions): (keyId: string) => Promise<KeyObject | undefined>;
|
|
95
|
+
export interface OxyRequesterAssertionIntrospection {
|
|
96
|
+
readonly active: boolean;
|
|
97
|
+
readonly requesterAccountId?: string;
|
|
98
|
+
readonly agentId?: string;
|
|
99
|
+
readonly applicationId?: string;
|
|
100
|
+
readonly credentialId?: string;
|
|
101
|
+
readonly jti?: string;
|
|
102
|
+
readonly expiresAt?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface OxyRequesterAssertionIntrospector {
|
|
105
|
+
introspectRequesterAssertion(input: {
|
|
106
|
+
assertion: string;
|
|
107
|
+
presenter: {
|
|
108
|
+
applicationId: string;
|
|
109
|
+
credentialId: string;
|
|
110
|
+
};
|
|
111
|
+
}): Promise<OxyRequesterAssertionIntrospection>;
|
|
112
|
+
getBaseURL(): string;
|
|
113
|
+
}
|
|
114
|
+
/** What the audience learns about the present requester. */
|
|
115
|
+
export interface OxyRequesterContext {
|
|
116
|
+
readonly userId: string;
|
|
117
|
+
readonly agentId: string;
|
|
118
|
+
readonly applicationId: string;
|
|
119
|
+
readonly credentialId: string;
|
|
120
|
+
readonly jti: string;
|
|
121
|
+
readonly expiresAt: string;
|
|
122
|
+
}
|
|
123
|
+
export interface OxyRequesterAssertionRequest extends OxyAuthRequest {
|
|
124
|
+
oxyRequester?: OxyRequesterContext;
|
|
125
|
+
}
|
|
126
|
+
export interface OxyRequesterAssertionAuthOptions {
|
|
127
|
+
/** This service's audience name, e.g. `alia`. */
|
|
128
|
+
readonly audience: string;
|
|
129
|
+
/** Defaults to `https://api.oxy.so`. */
|
|
130
|
+
readonly issuer?: string;
|
|
131
|
+
/** Defaults to `/capabilities/.well-known/jwks.json` on the introspector's origin. */
|
|
132
|
+
readonly jwksUrl?: string;
|
|
133
|
+
readonly resolvePublicKey?: (keyId: string) => Promise<KeyObject | undefined>;
|
|
134
|
+
readonly now?: () => Date;
|
|
135
|
+
/** Observability hook. Receives ids only, never the assertion. */
|
|
136
|
+
readonly onRejected?: (event: {
|
|
137
|
+
readonly code: string;
|
|
138
|
+
readonly status: number;
|
|
139
|
+
readonly applicationId: string | null;
|
|
140
|
+
}) => void;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Accepts an `X-Oxy-Requester-Assertion` presented beside a VERIFIED service
|
|
144
|
+
* token. Mount it after `createOxyAuthMiddleware`. Without the header it does
|
|
145
|
+
* nothing, so it composes with every other lane.
|
|
146
|
+
*
|
|
147
|
+
* `introspector` must be an `OxyServices` configured with THIS service's own
|
|
148
|
+
* credential (`configureServiceAuth`): Oxy only lets the audience's application
|
|
149
|
+
* consume an assertion.
|
|
150
|
+
*/
|
|
151
|
+
export declare function createOxyRequesterAssertionAuth(introspector: OxyRequesterAssertionIntrospector, options: OxyRequesterAssertionAuthOptions): RequestHandler;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -97,6 +97,25 @@ export interface CapabilityExecutionAuthorization {
|
|
|
97
97
|
updatedAt: string;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** A present-requester assertion minted for a product backend (ADR 0025). */
|
|
101
|
+
export interface RequesterAssertionGrant {
|
|
102
|
+
assertion: string;
|
|
103
|
+
expiresAt: string;
|
|
104
|
+
requesterAccountId: string;
|
|
105
|
+
agentId: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Oxy's live answer to the audience that consumed an assertion (ADR 0025). */
|
|
109
|
+
export interface RequesterAssertionIntrospection {
|
|
110
|
+
active: boolean;
|
|
111
|
+
requesterAccountId?: string;
|
|
112
|
+
agentId?: string;
|
|
113
|
+
applicationId?: string;
|
|
114
|
+
credentialId?: string;
|
|
115
|
+
jti?: string;
|
|
116
|
+
expiresAt?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
function query(path: string, key: string, value: string): string {
|
|
101
120
|
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
102
121
|
}
|
|
@@ -273,5 +292,62 @@ export function OxyServicesAgencyMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
273
292
|
throw this.handleError(error);
|
|
274
293
|
}
|
|
275
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Product backend → Oxy: trade the signed-in requester's access token for a
|
|
298
|
+
* one-use assertion naming `agentId` (ADR 0025). Authenticated with THIS
|
|
299
|
+
* instance's service credential (`configureServiceAuth`). `subjectToken` is
|
|
300
|
+
* sent to Oxy only, in the body; never forward it anywhere else.
|
|
301
|
+
*
|
|
302
|
+
* Not retried: a refusal is an answer, and the caller mints per turn.
|
|
303
|
+
*/
|
|
304
|
+
async mintRequesterAssertion(input: {
|
|
305
|
+
agentId: string;
|
|
306
|
+
subjectToken: string;
|
|
307
|
+
}): Promise<RequesterAssertionGrant> {
|
|
308
|
+
try {
|
|
309
|
+
const serviceToken = await (this as unknown as { getServiceToken(): Promise<string> }).getServiceToken();
|
|
310
|
+
return await this.makeRequest<RequesterAssertionGrant>(
|
|
311
|
+
'POST',
|
|
312
|
+
'/internal/native-agents/requester-assertions',
|
|
313
|
+
{ agentId: input.agentId, subjectToken: input.subjectToken },
|
|
314
|
+
{
|
|
315
|
+
cache: false,
|
|
316
|
+
retry: false,
|
|
317
|
+
timeout: 5000,
|
|
318
|
+
headers: { Authorization: `Bearer ${serviceToken}` },
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
throw this.handleError(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Audience → Oxy: verify, live-revalidate and CONSUME an assertion. A second
|
|
328
|
+
* call with the same assertion answers `active: false`. Authenticated with
|
|
329
|
+
* the audience's own service credential.
|
|
330
|
+
*/
|
|
331
|
+
async introspectRequesterAssertion(input: {
|
|
332
|
+
assertion: string;
|
|
333
|
+
presenter: { applicationId: string; credentialId: string };
|
|
334
|
+
}): Promise<RequesterAssertionIntrospection> {
|
|
335
|
+
try {
|
|
336
|
+
const serviceToken = await (this as unknown as { getServiceToken(): Promise<string> }).getServiceToken();
|
|
337
|
+
return await this.makeRequest<RequesterAssertionIntrospection>(
|
|
338
|
+
'POST',
|
|
339
|
+
'/internal/native-agents/requester-assertions/introspect',
|
|
340
|
+
{ assertion: input.assertion, presenter: input.presenter },
|
|
341
|
+
{
|
|
342
|
+
cache: false,
|
|
343
|
+
retry: false,
|
|
344
|
+
timeout: 5000,
|
|
345
|
+
headers: { Authorization: `Bearer ${serviceToken}` },
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw this.handleError(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
276
352
|
};
|
|
277
353
|
}
|