@oxy.so/core 1.3.1 → 1.5.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/mixins/OxyServices.utility.js +173 -6
- package/dist/cjs/server/auth.js +29 -0
- package/dist/cjs/server/index.js +17 -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/mixins/OxyServices.utility.js +173 -6
- package/dist/esm/server/auth.js +28 -0
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/requesterAssertion.js +353 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.agency.d.ts +41 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +49 -0
- package/dist/types/server/auth.d.ts +19 -1
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/requesterAssertion.d.ts +151 -0
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/mixins/OxyServices.agency.ts +76 -0
- package/src/mixins/OxyServices.utility.ts +226 -5
- package/src/mixins/__tests__/authRefusalObservability.test.ts +396 -0
- package/src/server/__tests__/requesterAssertion.test.ts +359 -0
- package/src/server/auth.ts +37 -1
- package/src/server/index.ts +29 -0
- package/src/server/requesterAssertion.ts +465 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type { CommonsDeliveryFacts, CommonsDeliveryPlatform, CommonsDeliveryRout
|
|
|
28
28
|
export type { ServiceTokenResponse, OAuthUserInfoResponse, OAuthTokenExchangeResult, } from './mixins/OxyServices.auth';
|
|
29
29
|
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult, CommonsOAuthFinalizeResult, CommonsDeliveryResult, } from './mixins/OxyServices.auth';
|
|
30
30
|
export type { PushTokenPlatform, RegisterPushTokenInput, } from './mixins/OxyServices.notifications';
|
|
31
|
-
export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
|
|
31
|
+
export type { ServiceApp, ServiceActingAsVerification, OxyAuthRefusal } from './mixins/OxyServices.utility';
|
|
32
32
|
export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
|
|
33
33
|
export type { InitDeviceTransferResult, DeviceTransferOutcome, } from './mixins/OxyServices.deviceTransfer';
|
|
34
34
|
export type { BulkFollowEntry, BulkFollowResult, BulkUnfollowEntry, BulkUnfollowResult, FollowMutationResult, ViewerGraph, } from './mixins/OxyServices.user';
|
|
@@ -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;
|
|
@@ -58,6 +58,43 @@ export interface ServiceApp {
|
|
|
58
58
|
/** Test/live isolation (F2.0): which `ApplicationCredential.environment` minted this token. */
|
|
59
59
|
environment: OxyServiceEnvironment;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Why a request's credential was refused, recorded on the request itself.
|
|
63
|
+
*
|
|
64
|
+
* The middleware answers a fixed, deliberately uninformative body to the
|
|
65
|
+
* client; this is the other half of that trade. A refusal is ALWAYS observable
|
|
66
|
+
* to the HOST: it is logged at `warn` with a stable `code`, handed to
|
|
67
|
+
* `onRefusal`, and left on `req.oxyAuthRefusal` for the host's own logs.
|
|
68
|
+
*
|
|
69
|
+
* It exists because the opposite cost was measured: Oxy served
|
|
70
|
+
* `{"keys":[]}` from `/.well-known/jwks.json` with no Ed25519 signing key
|
|
71
|
+
* bound, every service token failed `Oxy service-token key set is unavailable`,
|
|
72
|
+
* and on the OPTIONAL path that failure fell straight through to `next()`.
|
|
73
|
+
* The host then answered its own generic 401 and logged nothing at all, so the
|
|
74
|
+
* one fact that named the fault — an empty key set — existed nowhere. Hours.
|
|
75
|
+
*
|
|
76
|
+
* **Nothing here is ever sent to a client, and `reason` never contains the
|
|
77
|
+
* token, a signature, a secret or a session id.** It carries the SDK's own
|
|
78
|
+
* failure message (`kid` unknown, audience mismatch, key set unavailable) plus
|
|
79
|
+
* the ids already considered safe to log elsewhere in this middleware.
|
|
80
|
+
*/
|
|
81
|
+
export interface OxyAuthRefusal {
|
|
82
|
+
/** Stable, greppable code. Matches the `code` a non-optional refusal answers. */
|
|
83
|
+
code: string;
|
|
84
|
+
/** Which credential lane refused. */
|
|
85
|
+
stage: 'token' | 'service-token' | 'session';
|
|
86
|
+
/** Human-readable cause. Never a token, signature, secret or session id. */
|
|
87
|
+
reason: string;
|
|
88
|
+
/** The status a NON-optional mount would have answered with. */
|
|
89
|
+
status: number;
|
|
90
|
+
/**
|
|
91
|
+
* `true` when the middleware was mounted with `optional: true`, so this
|
|
92
|
+
* refusal did not produce a response — the request continued unauthenticated
|
|
93
|
+
* and whatever the host does next (typically its own generic 401) is the
|
|
94
|
+
* only thing the client sees.
|
|
95
|
+
*/
|
|
96
|
+
optional: boolean;
|
|
97
|
+
}
|
|
61
98
|
/**
|
|
62
99
|
* Options for oxyClient.auth() middleware
|
|
63
100
|
*/
|
|
@@ -70,6 +107,17 @@ interface AuthMiddlewareOptions {
|
|
|
70
107
|
loadUser?: boolean;
|
|
71
108
|
/** Optional auth - attach user if token present but don't block (default: false) */
|
|
72
109
|
optional?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Called whenever a PRESENTED credential is refused, on the optional path as
|
|
112
|
+
* well as the blocking one. Runs before the response (if any) and after
|
|
113
|
+
* `req.oxyAuthRefusal` is set, so a host can raise its own alert or carry the
|
|
114
|
+
* code into its request log. Throwing from it is swallowed — an observer must
|
|
115
|
+
* not be able to turn a refusal into a 500.
|
|
116
|
+
*
|
|
117
|
+
* Refusal is about a credential that was offered and rejected: a request with
|
|
118
|
+
* NO `Authorization` header is not refused and does not reach this.
|
|
119
|
+
*/
|
|
120
|
+
onRefusal?: (refusal: OxyAuthRefusal) => void;
|
|
73
121
|
/**
|
|
74
122
|
* Public JWKS endpoint used for Ed25519 service-token verification. Defaults
|
|
75
123
|
* to `/.well-known/jwks.json` on this Oxy client's configured API origin.
|
|
@@ -362,6 +410,7 @@ interface AuthReq {
|
|
|
362
410
|
userId: string;
|
|
363
411
|
scopes: string[];
|
|
364
412
|
};
|
|
413
|
+
oxyAuthRefusal?: OxyAuthRefusal;
|
|
365
414
|
}
|
|
366
415
|
interface AuthRes {
|
|
367
416
|
status(code: number): AuthRes;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
2
2
|
import type { OxyServices } from '../OxyServices';
|
|
3
|
+
import type { OxyAuthRefusal } from '../mixins/OxyServices.utility';
|
|
3
4
|
import { OXY_SERVICE_ENVIRONMENTS, type OxyServiceEnvironment } from '../utils/oxyServiceEnvironment';
|
|
4
5
|
export { OXY_SERVICE_ENVIRONMENTS };
|
|
5
|
-
export type { OxyServiceEnvironment };
|
|
6
|
+
export type { OxyServiceEnvironment, OxyAuthRefusal };
|
|
6
7
|
export interface OxyRequestUser {
|
|
7
8
|
id: string;
|
|
8
9
|
_id?: string;
|
|
@@ -35,6 +36,12 @@ export interface OxyAuthRequest extends Request {
|
|
|
35
36
|
sessionId?: string | null;
|
|
36
37
|
serviceApp?: OxyServiceAppContext;
|
|
37
38
|
serviceActingAs?: OxyServiceActingAsContext;
|
|
39
|
+
/**
|
|
40
|
+
* Why the presented credential was refused, when one was and the request
|
|
41
|
+
* still reached the handler (the optional path). Read it with
|
|
42
|
+
* {@link getOxyAuthRefusal} — never put it in a response body.
|
|
43
|
+
*/
|
|
44
|
+
oxyAuthRefusal?: OxyAuthRefusal;
|
|
38
45
|
}
|
|
39
46
|
export interface OxyAuthenticatedRequest extends OxyAuthRequest {
|
|
40
47
|
userId: string;
|
|
@@ -125,6 +132,17 @@ export declare function getOxyDelegatedUserId(req: Request): string | null;
|
|
|
125
132
|
*/
|
|
126
133
|
export declare function getOxyRequestAttribution(req: Request): OxyRequestAttribution | null;
|
|
127
134
|
export declare function getRequiredOxyUserId(req: Request): string;
|
|
135
|
+
/**
|
|
136
|
+
* Why the credential on this request was refused, or `null` when none was
|
|
137
|
+
* presented (or it was accepted).
|
|
138
|
+
*
|
|
139
|
+
* Set by `oxy.auth()` — including the `optional: true` mount the composed
|
|
140
|
+
* helpers use, where a refusal otherwise leaves no trace: the request simply
|
|
141
|
+
* arrives unauthenticated and the host answers its own generic 401. Hosts log
|
|
142
|
+
* this beside their own 401; it is diagnostic and **must not be put into a
|
|
143
|
+
* response body**, which is exactly what it is kept out of.
|
|
144
|
+
*/
|
|
145
|
+
export declare function getOxyAuthRefusal(req: Request): OxyAuthRefusal | null;
|
|
128
146
|
export declare function requireOxyAuth(req: Request, res: Response, next: NextFunction): void;
|
|
129
147
|
export declare function createOptionalOxyAuth(oxy: OxyServices, options?: OxyAuthMiddlewareOptions): RequestHandler;
|
|
130
148
|
export declare function createOxyAuthMiddleware(oxy: OxyServices, options?: OxyAuthMiddlewareOptions): RequestHandler;
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* app.use(createOxyRateLimit(oxy, { store: redisStore }));
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth';
|
|
18
|
-
export type { OxyAuthenticatedRequest, OxyAuthMiddlewareOptions, OxyAuthRequest, OxyBillingPrincipal, OxyRequestAttribution, OxyRequestUser, OxyServiceActingAsContext, OxyServiceAppContext, OxyServiceEnvironment, } from './auth';
|
|
17
|
+
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyAuthRefusal, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth';
|
|
18
|
+
export type { OxyAuthenticatedRequest, OxyAuthRefusal, OxyAuthMiddlewareOptions, OxyAuthRequest, OxyBillingPrincipal, OxyRequestAttribution, OxyRequestUser, OxyServiceActingAsContext, OxyServiceAppContext, OxyServiceEnvironment, } from './auth';
|
|
19
19
|
export { createOxyRateLimit } from './rateLimit';
|
|
20
20
|
export type { OxyRateLimitOptions } from './rateLimit';
|
|
21
21
|
export { assertSafePublicUrl, isBlockedIp, safeFetch, SsrfRejection, UpstreamError, ALLOWED_PORTS, ALLOWED_PROTOCOLS, BLOCKED_HOSTNAMES, DEFAULT_USER_AGENT, MAX_REDIRECTS, MAX_URL_LENGTH, UPSTREAM_HEADERS_TIMEOUT_MS, } from './safeFetch';
|
|
@@ -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
|
@@ -81,7 +81,7 @@ export type {
|
|
|
81
81
|
PushTokenPlatform,
|
|
82
82
|
RegisterPushTokenInput,
|
|
83
83
|
} from './mixins/OxyServices.notifications';
|
|
84
|
-
export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
|
|
84
|
+
export type { ServiceApp, ServiceActingAsVerification, OxyAuthRefusal } from './mixins/OxyServices.utility';
|
|
85
85
|
export type {
|
|
86
86
|
ContactDiscoveryMatch,
|
|
87
87
|
ContactDiscoveryResponse,
|
|
@@ -136,6 +136,8 @@ export type {
|
|
|
136
136
|
DelegationCatalogBinding,
|
|
137
137
|
DelegationGrantView,
|
|
138
138
|
PutAccountCapabilityPolicyInput,
|
|
139
|
+
RequesterAssertionGrant,
|
|
140
|
+
RequesterAssertionIntrospection,
|
|
139
141
|
UpdateDelegationGrantInput,
|
|
140
142
|
} from './mixins/OxyServices.agency';
|
|
141
143
|
|
|
@@ -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
|
}
|