@oxy.so/core 7.16.0 → 7.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -13
- package/dist/cjs/server/OxyServer.js +85 -3
- package/dist/cjs/server/appPermissions.js +14 -92
- package/dist/cjs/server/callerAccess.js +151 -0
- package/dist/cjs/server/callerAuthority.js +206 -0
- package/dist/cjs/server/capabilityTicket.js +39 -6
- package/dist/cjs/server/cors.js +41 -2
- package/dist/cjs/server/index.js +33 -5
- package/dist/cjs/server/liveCapabilityTicket.js +4 -2
- package/dist/cjs/server/middleware.js +104 -2
- package/dist/cjs/server/namespaces.js +15 -4
- package/dist/cjs/server/safeFetch.js +29 -1
- package/dist/cjs/server/userClient.js +127 -0
- package/dist/cjs/utils/cache.js +58 -22
- package/dist/esm/server/OxyServer.js +85 -3
- package/dist/esm/server/appPermissions.js +12 -89
- package/dist/esm/server/callerAccess.js +145 -0
- package/dist/esm/server/callerAuthority.js +194 -0
- package/dist/esm/server/capabilityTicket.js +38 -6
- package/dist/esm/server/cors.js +39 -2
- package/dist/esm/server/index.js +26 -3
- package/dist/esm/server/liveCapabilityTicket.js +5 -3
- package/dist/esm/server/middleware.js +104 -2
- package/dist/esm/server/namespaces.js +15 -4
- package/dist/esm/server/safeFetch.js +29 -1
- package/dist/esm/server/userClient.js +121 -0
- package/dist/esm/utils/cache.js +57 -22
- package/dist/types/api/accounts.d.ts +8 -1
- package/dist/types/api/apps.d.ts +22 -4
- package/dist/types/index.d.ts +1 -1
- package/dist/types/server/OxyServer.d.ts +74 -7
- package/dist/types/server/appPermissions.d.ts +3 -13
- package/dist/types/server/auth.d.ts +5 -0
- package/dist/types/server/callerAccess.d.ts +86 -0
- package/dist/types/server/callerAuthority.d.ts +127 -0
- package/dist/types/server/capabilityTicket.d.ts +23 -1
- package/dist/types/server/cors.d.ts +39 -0
- package/dist/types/server/index.d.ts +31 -7
- package/dist/types/server/liveCapabilityTicket.d.ts +6 -2
- package/dist/types/server/middleware.d.ts +34 -2
- package/dist/types/server/namespaces.d.ts +18 -2
- package/dist/types/server/safeFetch.d.ts +10 -0
- package/dist/types/server/userClient.d.ts +71 -0
- package/dist/types/utils/cache.d.ts +13 -0
- package/package.json +1 -1
- package/src/api/accounts.ts +8 -1
- package/src/api/apps.ts +23 -4
- package/src/index.ts +1 -0
- package/src/server/OxyServer.ts +126 -7
- package/src/server/appPermissions.ts +28 -105
- package/src/server/auth.ts +5 -0
- package/src/server/callerAccess.ts +232 -0
- package/src/server/callerAuthority.ts +267 -0
- package/src/server/capabilityTicket.ts +76 -17
- package/src/server/cors.ts +63 -2
- package/src/server/index.ts +38 -4
- package/src/server/liveCapabilityTicket.ts +12 -5
- package/src/server/middleware.ts +171 -4
- package/src/server/namespaces.ts +36 -5
- package/src/server/safeFetch.ts +31 -1
- package/src/server/userClient.ts +147 -0
- package/src/utils/cache.ts +56 -18
|
@@ -8,6 +8,9 @@ import { type WorkloadCredentialProvider } from './workloadIdentity';
|
|
|
8
8
|
import { type OxyAccountEvent, type OxyAccountEventFeedPage, type OxyMiddleware, type ServiceActingAsVerification, type VerifyAccountEventOptions } from './middleware';
|
|
9
9
|
import { type AppPermissionsOptions, type AppPermissionsRequest, type OxyAppPermissions } from './appPermissions';
|
|
10
10
|
import { ServerAccountsApi, ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi } from './namespaces';
|
|
11
|
+
import { type CallerAccessOptions, type OxyAccountAccess, type OxyApplicationAccess } from './callerAccess';
|
|
12
|
+
import type { OxyCallerRequest } from './callerAuthority';
|
|
13
|
+
import { OxyUserClient, type OxyUserSource } from './userClient';
|
|
11
14
|
import { ServerSafetyApi } from './safety';
|
|
12
15
|
import { ServerSessionApi } from './session';
|
|
13
16
|
export interface OxyServerConfig extends OxyConfig {
|
|
@@ -47,12 +50,21 @@ export interface OxyServerConfig extends OxyConfig {
|
|
|
47
50
|
*/
|
|
48
51
|
applicationId?: string;
|
|
49
52
|
/**
|
|
50
|
-
* How long one
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* How long one caller-authority answer — `appPermissionsFor`,
|
|
54
|
+
* `accountAccessFor`, `applicationAccessFor` and their middleware — is
|
|
55
|
+
* reused, per session (default 30 000 ms). This is the longest a revoked
|
|
56
|
+
* grant keeps working on this backend for a read that does not pass
|
|
57
|
+
* `fresh: true`. `0` disables the cache. Only positive answers are kept.
|
|
54
58
|
*/
|
|
55
59
|
appPermissionsCacheTtlMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* The service-token audience this backend accepts when a mount names none:
|
|
62
|
+
* its own `oxy-service:<applicationId>`. Every `middleware.auth()` /
|
|
63
|
+
* `middleware.service()` mount — and so `createOxyAuthMiddleware`,
|
|
64
|
+
* `createOptionalOxyAuth` and `createOxyRateLimit` — uses it unless it
|
|
65
|
+
* passes `expectedAudience` itself. Unset, they accept `oxy-api` (unchanged).
|
|
66
|
+
*/
|
|
67
|
+
expectedAudience?: string;
|
|
56
68
|
/**
|
|
57
69
|
* How long one successful user-bearer validation (`session.validate` with an
|
|
58
70
|
* `accessToken`, which `middleware.auth()` runs on every request) is reused
|
|
@@ -168,6 +180,7 @@ export declare class OxyServer extends OxyServices {
|
|
|
168
180
|
private readonly configuredApplicationId;
|
|
169
181
|
private readonly sessionValidationTtlMs;
|
|
170
182
|
private readonly appPermissions;
|
|
183
|
+
private readonly callerAccess;
|
|
171
184
|
/** Opened the first time the validation cache would be used; see `./serviceEvents.ts`. */
|
|
172
185
|
private serviceEventFeed;
|
|
173
186
|
private disposed;
|
|
@@ -306,13 +319,67 @@ export declare class OxyServer extends OxyServices {
|
|
|
306
319
|
*/
|
|
307
320
|
appPermissionsFor(req: AppPermissionsRequest, options?: AppPermissionsOptions): Promise<OxyAppPermissions>;
|
|
308
321
|
/**
|
|
309
|
-
* Drop cached
|
|
310
|
-
*
|
|
311
|
-
*
|
|
322
|
+
* Drop cached caller-authority answers — application permissions and
|
|
323
|
+
* account / application access — all of them, or one person's (as the
|
|
324
|
+
* session user or the resolved operator). For a backend that learns of a
|
|
325
|
+
* revocation out of band and will not wait out the TTL.
|
|
312
326
|
*/
|
|
313
327
|
invalidateAppPermissions(filter?: {
|
|
314
328
|
userId?: string;
|
|
315
329
|
}): void;
|
|
330
|
+
/**
|
|
331
|
+
* An isolated Oxy client authenticated as the person behind `source` — a
|
|
332
|
+
* request `middleware.auth()` resolved to a user session, or a bearer you
|
|
333
|
+
* already verified. Every `OxyServices` namespace, with no response cache,
|
|
334
|
+
* no token refresh and no fallback to this server's service identity; this
|
|
335
|
+
* server's own session is never touched.
|
|
336
|
+
*
|
|
337
|
+
* Prefer {@link withUser}, which disposes it for you. A client from here
|
|
338
|
+
* should be disposed when the request ends (`client.dispose()`) so a stray
|
|
339
|
+
* reference cannot keep using the bearer; one that is not holds no global
|
|
340
|
+
* resource and is collected normally.
|
|
341
|
+
*
|
|
342
|
+
* Throws `OxyAccessError` (`ACCESS_SESSION_REQUIRED`) for a request with no
|
|
343
|
+
* user session, including one authenticated by a service token.
|
|
344
|
+
*/
|
|
345
|
+
asUser(source: OxyUserSource): OxyUserClient;
|
|
346
|
+
/**
|
|
347
|
+
* Run `use` with a client authenticated as the person behind `source`
|
|
348
|
+
* ({@link asUser}), and dispose it when `use` settles — success or failure.
|
|
349
|
+
*
|
|
350
|
+
* ```ts
|
|
351
|
+
* const accounts = await oxy.withUser(req, (user) => user.accounts.list());
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
withUser<T>(source: OxyUserSource, use: (user: OxyUserClient) => T | Promise<T>): Promise<T>;
|
|
355
|
+
/**
|
|
356
|
+
* What the person behind `req` may do in Oxy account `accountId` — an
|
|
357
|
+
* organization, store, channel or their own personal account: relationship,
|
|
358
|
+
* role, source and the EFFECTIVE permissions (inheritance, grants and
|
|
359
|
+
* revokes applied). Read with the request's own verified bearer.
|
|
360
|
+
*
|
|
361
|
+
* Cached per session for `appPermissionsCacheTtlMs` (30 s), positive
|
|
362
|
+
* answers only; pass `{ fresh: true }` before a write. Throws
|
|
363
|
+
* `OxyAccessError`: `ACCESS_SESSION_REQUIRED` (no session, a service token,
|
|
364
|
+
* or Oxy answered 401), `ACCESS_DENIED` (Oxy answered 403/404 — no access,
|
|
365
|
+
* or no such account), `ACCESS_UNAVAILABLE` (anything else; never a "no").
|
|
366
|
+
* Map it with `accessErrorStatus`. Gate on `has(permission)`, never on
|
|
367
|
+
* `role`.
|
|
368
|
+
*/
|
|
369
|
+
accountAccessFor(req: OxyCallerRequest, accountId: string, options?: CallerAccessOptions): Promise<OxyAccountAccess>;
|
|
370
|
+
/**
|
|
371
|
+
* What the person behind `req` may do to Oxy application `applicationId`,
|
|
372
|
+
* through their membership of the account that owns it (developer RBAC:
|
|
373
|
+
* `app:read`, `app:update`, `webhooks:update`, `billing:manage`, …), plus
|
|
374
|
+
* its `ownerAccountId` and whether it is `official`. Read with the
|
|
375
|
+
* request's own verified bearer.
|
|
376
|
+
*
|
|
377
|
+
* Not {@link appPermissionsFor}: that one answers which of THIS backend's
|
|
378
|
+
* own declared operator keys the person holds. Same cache, options and
|
|
379
|
+
* errors as {@link accountAccessFor}; Oxy refuses (403) a caller with no
|
|
380
|
+
* `app:read`, so a successful answer always includes it.
|
|
381
|
+
*/
|
|
382
|
+
applicationAccessFor(req: OxyCallerRequest, applicationId: string, options?: CallerAccessOptions): Promise<OxyApplicationAccess>;
|
|
316
383
|
/**
|
|
317
384
|
* Drop cached session validations — all of them, one user's or one
|
|
318
385
|
* session's. Oxy's revocation feed does this by itself; a host may also call
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*
|
|
44
44
|
* Node only; reachable solely from `@oxy.so/core/server`.
|
|
45
45
|
*/
|
|
46
|
+
import { authorityErrorBody, type OxyCallerRequest } from './callerAuthority';
|
|
46
47
|
/** Default cache lifetime of one answer. */
|
|
47
48
|
export declare const APP_PERMISSIONS_CACHE_TTL_MS = 30000;
|
|
48
49
|
/** What a refused request is answered with. */
|
|
@@ -105,22 +106,13 @@ export interface RequireAppPermissionOptions extends AppPermissionsOptions {
|
|
|
105
106
|
respond?: boolean;
|
|
106
107
|
}
|
|
107
108
|
/** The request fields the resolver reads — all set by `middleware.auth()`. */
|
|
108
|
-
export interface AppPermissionsRequest {
|
|
109
|
-
userId?: string | null;
|
|
110
|
-
sessionId?: string | null;
|
|
111
|
-
accessToken?: string;
|
|
112
|
-
serviceApp?: unknown;
|
|
109
|
+
export interface AppPermissionsRequest extends OxyCallerRequest {
|
|
113
110
|
oxyAppPermissions?: OxyAppPermissions;
|
|
114
111
|
}
|
|
115
112
|
/** The status the middleware answers an error with, under `deny`. */
|
|
116
113
|
export declare function appPermissionErrorStatus(error: OxyAppPermissionError, deny?: OxyAppPermissionDenyStatus): number;
|
|
117
114
|
/** The response body for a status: the API error envelope (`docs/api-conventions.md`). */
|
|
118
|
-
export declare
|
|
119
|
-
error: {
|
|
120
|
-
code: string;
|
|
121
|
-
message: string;
|
|
122
|
-
};
|
|
123
|
-
};
|
|
115
|
+
export declare const appPermissionErrorBody: typeof authorityErrorBody;
|
|
124
116
|
/** Throws at MOUNT time on a key that could never be held — a typo is a deploy error. */
|
|
125
117
|
export declare function assertApplicationPermissionKey(permission: unknown): asserts permission is string;
|
|
126
118
|
/** What the resolver needs from the server it belongs to. */
|
|
@@ -138,7 +130,6 @@ export interface AppPermissionsHost {
|
|
|
138
130
|
export declare class OxyAppPermissionsResolver {
|
|
139
131
|
private readonly host;
|
|
140
132
|
private readonly cache;
|
|
141
|
-
private readonly pending;
|
|
142
133
|
constructor(host: AppPermissionsHost);
|
|
143
134
|
/**
|
|
144
135
|
* What the person behind `req` holds. Throws {@link OxyAppPermissionError};
|
|
@@ -152,6 +143,5 @@ export declare class OxyAppPermissionsResolver {
|
|
|
152
143
|
invalidate(filter?: {
|
|
153
144
|
userId?: string;
|
|
154
145
|
}): void;
|
|
155
|
-
private remember;
|
|
156
146
|
private lookup;
|
|
157
147
|
}
|
|
@@ -2,6 +2,7 @@ import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
|
2
2
|
import type { AuthMiddlewareOptions, OxyAuthRefusal, OxyMiddleware, ServiceApp } from './middleware';
|
|
3
3
|
import type { AccountActorChain } from '@oxy.so/contracts';
|
|
4
4
|
import type { OxyAppPermissions } from './appPermissions';
|
|
5
|
+
import type { OxyAccountAccess, OxyApplicationAccess } from './callerAccess';
|
|
5
6
|
import { OXY_SERVICE_ENVIRONMENTS, type OxyServiceEnvironment } from '../utils/oxyServiceEnvironment';
|
|
6
7
|
export { OXY_SERVICE_ENVIRONMENTS };
|
|
7
8
|
export type { OxyServiceEnvironment, OxyAuthRefusal };
|
|
@@ -49,6 +50,10 @@ export interface OxyAuthRequest extends Request {
|
|
|
49
50
|
* request. Its `operatorId` is the human to record as the actor.
|
|
50
51
|
*/
|
|
51
52
|
oxyAppPermissions?: OxyAppPermissions;
|
|
53
|
+
/** Set by `middleware.requireAccountAccess` once it admits the request. */
|
|
54
|
+
oxyAccountAccess?: OxyAccountAccess;
|
|
55
|
+
/** Set by `middleware.requireApplicationAccess` once it admits the request. */
|
|
56
|
+
oxyApplicationAccess?: OxyApplicationAccess;
|
|
52
57
|
}
|
|
53
58
|
export interface OxyAuthenticatedRequest extends OxyAuthRequest {
|
|
54
59
|
userId: string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person behind a request, in an Oxy account and over an Oxy application:
|
|
3
|
+
* `OxyServer.accountAccessFor` and `OxyServer.applicationAccessFor`.
|
|
4
|
+
*
|
|
5
|
+
* Both read Oxy with the request's OWN verified bearer (an `OxyUserClient`),
|
|
6
|
+
* so Oxy resolves the HUMAN operator from the session — a person acting as
|
|
7
|
+
* their organization is answered for themselves — with inheritance, per-member
|
|
8
|
+
* grants and revokes applied. Nothing else the client sends is consulted.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Can the caller update this store (an Oxy account) in my app?
|
|
12
|
+
* const access = await oxy.accountAccessFor(req, storeAccountId, { fresh: true });
|
|
13
|
+
* if (!access.has('account:update')) throw new OxyAccessError('ACCESS_DENIED', '…');
|
|
14
|
+
*
|
|
15
|
+
* // Can the caller manage webhooks of application A (developer RBAC)?
|
|
16
|
+
* const app = await oxy.applicationAccessFor(req, applicationId);
|
|
17
|
+
* if (!app.has('webhooks:update')) …
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Which reader answers which question is in `./callerAuthority`; the short
|
|
21
|
+
* version: `appPermissionsFor` is about THIS backend's own declared operator
|
|
22
|
+
* keys, `applicationAccessFor` about any application through the account that
|
|
23
|
+
* owns it, `accountAccessFor` about an account. For work with no person
|
|
24
|
+
* present (a background import), `oxy.accounts.accessFor(accountId, userId)`
|
|
25
|
+
* asks with the service's own token instead.
|
|
26
|
+
*
|
|
27
|
+
* Node only; reachable solely from `@oxy.so/core/server`.
|
|
28
|
+
*/
|
|
29
|
+
import { type AccountAccessSource, type AccountKind } from '@oxy.so/contracts';
|
|
30
|
+
import type { AccountRelationship } from '../api/accounts';
|
|
31
|
+
import { type OxyCallerRequest } from './callerAuthority';
|
|
32
|
+
/** What the person behind a request may do in one Oxy account. */
|
|
33
|
+
export interface OxyAccountAccess {
|
|
34
|
+
readonly accountId: string;
|
|
35
|
+
readonly kind: AccountKind;
|
|
36
|
+
/** `self` for their own personal account, `owner`, or `member` (direct or inherited). */
|
|
37
|
+
readonly relationship: AccountRelationship;
|
|
38
|
+
/** Informational; gate on `permissions` / `has`, never on the role name. */
|
|
39
|
+
readonly role: string;
|
|
40
|
+
/** Where the access comes from: `self`, a `direct` membership, or one `inherited` from an ancestor. */
|
|
41
|
+
readonly source: AccountAccessSource;
|
|
42
|
+
/** The EFFECTIVE account permissions (role baseline, grants, revokes), sorted. */
|
|
43
|
+
readonly permissions: readonly string[];
|
|
44
|
+
has(permission: string): boolean;
|
|
45
|
+
}
|
|
46
|
+
/** What the person behind a request may do to one Oxy application, through its owning account. */
|
|
47
|
+
export interface OxyApplicationAccess {
|
|
48
|
+
readonly applicationId: string;
|
|
49
|
+
/** The account that owns the application; the access derives from the caller's membership there. */
|
|
50
|
+
readonly ownerAccountId: string;
|
|
51
|
+
/** Whether the application is an official Oxy application. */
|
|
52
|
+
readonly official: boolean;
|
|
53
|
+
/** Informational; gate on `permissions` / `has`, never on the role name. */
|
|
54
|
+
readonly role: string;
|
|
55
|
+
/** The application permissions (`app:read`, `app:update`, `webhooks:update`, …), sorted. */
|
|
56
|
+
readonly permissions: readonly string[];
|
|
57
|
+
has(permission: string): boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface CallerAccessOptions {
|
|
60
|
+
/** Skip the cache and ask Oxy (the answer still refreshes the cache). Use before a write. */
|
|
61
|
+
fresh?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** What the readers need from the server they belong to. */
|
|
64
|
+
export interface CallerAccessHost {
|
|
65
|
+
/** `GET /accounts/:id` with this bearer, uncached. Rejects with the API error. */
|
|
66
|
+
readAccount(accountId: string, accessToken: string): Promise<unknown>;
|
|
67
|
+
/** `GET /applications/:id` with this bearer, uncached. Rejects with the API error. */
|
|
68
|
+
readApplication(applicationId: string, accessToken: string): Promise<unknown>;
|
|
69
|
+
readonly ttlMs: number;
|
|
70
|
+
}
|
|
71
|
+
/** `GET /accounts/:id` → the caller's effective access, or `null` for a malformed answer. */
|
|
72
|
+
export declare function parseAccountAccess(accountId: string, answer: unknown): OxyAccountAccess | null;
|
|
73
|
+
/** `GET /applications/:id` → the caller's access over it, or `null` for a malformed answer. */
|
|
74
|
+
export declare function parseApplicationAccess(applicationId: string, answer: unknown): OxyApplicationAccess | null;
|
|
75
|
+
/** The caches and lookups behind `accountAccessFor` / `applicationAccessFor`. One per server. */
|
|
76
|
+
export declare class OxyCallerAccessResolver {
|
|
77
|
+
private readonly host;
|
|
78
|
+
private readonly accounts;
|
|
79
|
+
private readonly applications;
|
|
80
|
+
constructor(host: CallerAccessHost);
|
|
81
|
+
account(req: OxyCallerRequest, accountId: string, options?: CallerAccessOptions): Promise<OxyAccountAccess>;
|
|
82
|
+
application(req: OxyCallerRequest, applicationId: string, options?: CallerAccessOptions): Promise<OxyApplicationAccess>;
|
|
83
|
+
invalidate(filter?: {
|
|
84
|
+
userId?: string;
|
|
85
|
+
}): void;
|
|
86
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every "may the person behind this request …?" read on `OxyServer`
|
|
3
|
+
* shares: which requests carry a person at all, the one failure type and its
|
|
4
|
+
* status mapping, the response envelope, and the positive-only answer cache.
|
|
5
|
+
*
|
|
6
|
+
* The readers built on it answer different questions — keep them apart:
|
|
7
|
+
*
|
|
8
|
+
* | Question | Reader |
|
|
9
|
+
* |---|---|
|
|
10
|
+
* | Does this person hold one of MY application's declared operator keys (`payments.operate`)? | `appPermissionsFor` (`./appPermissions`) |
|
|
11
|
+
* | What may this person do in Oxy account X (an organization, store, channel)? | `accountAccessFor` (`./callerAccess`) |
|
|
12
|
+
* | What may this person do to application A, through the account that owns it (`app:read`, `app:update`)? | `applicationAccessFor` (`./callerAccess`) |
|
|
13
|
+
*
|
|
14
|
+
* ## The outcome mapping, and why an outage is never a "no"
|
|
15
|
+
*
|
|
16
|
+
* Every reader asks Oxy with the request's OWN verified bearer and maps the
|
|
17
|
+
* answer the same way: a 401 means the session is gone (sign in again); a 403
|
|
18
|
+
* or 404 is Oxy's definitive refusal; anything else — a 5xx, a timeout, a
|
|
19
|
+
* malformed body — is UNAVAILABLE, answered 503. Only a positive answer is
|
|
20
|
+
* cached, so a blip can never be remembered as a refusal, and a refusal is
|
|
21
|
+
* never remembered at all (the next request asks again).
|
|
22
|
+
*
|
|
23
|
+
* Node only; reachable solely from `@oxy.so/core/server`.
|
|
24
|
+
*/
|
|
25
|
+
/** The request fields a caller-authority read uses — all set by `middleware.auth()`. */
|
|
26
|
+
export interface OxyCallerRequest {
|
|
27
|
+
userId?: string | null;
|
|
28
|
+
sessionId?: string | null;
|
|
29
|
+
accessToken?: string;
|
|
30
|
+
/** Set when the request is authenticated by a SERVICE token; such a request carries no person. */
|
|
31
|
+
serviceApp?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** The verified person behind a request. */
|
|
34
|
+
export interface OxyCallerSession {
|
|
35
|
+
readonly userId: string;
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
readonly accessToken: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The person behind `req`, or why there is none: `service` for a request
|
|
41
|
+
* authenticated by a service token (delegated or not — its bearer is the
|
|
42
|
+
* service's, never the person's), `missing` when `middleware.auth()` resolved
|
|
43
|
+
* no user session.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readCallerSession(req: OxyCallerRequest): {
|
|
46
|
+
ok: true;
|
|
47
|
+
session: OxyCallerSession;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: 'service' | 'missing';
|
|
51
|
+
};
|
|
52
|
+
/** A bearer that can be put in an `Authorization` header unchanged. */
|
|
53
|
+
export declare function isBearerToken(token: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* - `ACCESS_SESSION_REQUIRED` — no person on the request (anonymous, or a
|
|
56
|
+
* service token), or Oxy refused the session (401). Ask them to sign in.
|
|
57
|
+
* - `ACCESS_DENIED` — Oxy answered "no" (403, 404), or the answer lacks the
|
|
58
|
+
* permission a gate required. Definitive.
|
|
59
|
+
* - `ACCESS_UNAVAILABLE` — Oxy could not be asked or gave no usable answer.
|
|
60
|
+
* Fails closed, answered 503, never cached, never a "no".
|
|
61
|
+
*/
|
|
62
|
+
export type OxyAccessErrorCode = 'ACCESS_SESSION_REQUIRED' | 'ACCESS_DENIED' | 'ACCESS_UNAVAILABLE';
|
|
63
|
+
/** The typed failure of `asUser`, `accountAccessFor`, `applicationAccessFor` and their middleware. */
|
|
64
|
+
export declare class OxyAccessError extends Error {
|
|
65
|
+
readonly code: OxyAccessErrorCode;
|
|
66
|
+
/** The permission a gate required, when a gate refused. */
|
|
67
|
+
readonly permission: string | undefined;
|
|
68
|
+
/** The underlying failure (an `OxyApiError`), if any. Never sent to a client. */
|
|
69
|
+
readonly cause: unknown;
|
|
70
|
+
constructor(code: OxyAccessErrorCode, message: string, options?: {
|
|
71
|
+
permission?: string;
|
|
72
|
+
cause?: unknown;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** What a refused request is answered with. */
|
|
76
|
+
export type OxyAccessDenyStatus = 403 | 404;
|
|
77
|
+
/**
|
|
78
|
+
* The status for an {@link OxyAccessError}: `deny` (default `403`) for a
|
|
79
|
+
* refusal; `401` for no session (`404` when `deny` is `404`, so a hidden
|
|
80
|
+
* surface stays hidden from the signed-out too); `503` when Oxy could not say.
|
|
81
|
+
*/
|
|
82
|
+
export declare function accessErrorStatus(error: OxyAccessError, deny?: OxyAccessDenyStatus): 401 | 403 | 404 | 503;
|
|
83
|
+
/** The response body for a status: the API error envelope (`docs/api-conventions.md`). */
|
|
84
|
+
export declare function authorityErrorBody(status: number): {
|
|
85
|
+
error: {
|
|
86
|
+
code: string;
|
|
87
|
+
message: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/** The HTTP status an `OxyApiError` (or anything shaped like one) carries. */
|
|
91
|
+
export declare function errorStatus(error: unknown): number | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Map a failed caller-bearer read of Oxy to its outcome: 401 → session
|
|
94
|
+
* required, 403/404 → denied, everything else → unavailable.
|
|
95
|
+
*/
|
|
96
|
+
export declare function accessFailure(error: unknown, what: string): OxyAccessError;
|
|
97
|
+
/**
|
|
98
|
+
* The positive-only answer cache one caller-authority reader keeps, keyed by
|
|
99
|
+
* (resource, session, person). Concurrent identical reads share one lookup; a
|
|
100
|
+
* `fresh` read skips the cache and refreshes it; a slower, OLDER answer never
|
|
101
|
+
* overwrites a newer one (the newer may be the fresh read that saw a
|
|
102
|
+
* revocation); a failure is never stored.
|
|
103
|
+
*/
|
|
104
|
+
export declare class CallerAuthorityCache<T> {
|
|
105
|
+
private readonly ttlMs;
|
|
106
|
+
private readonly isAbout;
|
|
107
|
+
private readonly entries;
|
|
108
|
+
private readonly pending;
|
|
109
|
+
/**
|
|
110
|
+
* @param ttlMs How long an answer is reused; `0` disables the cache.
|
|
111
|
+
* @param isAbout Whether a cached answer concerns `userId` beyond the
|
|
112
|
+
* session's own user (an application-permissions answer names the
|
|
113
|
+
* resolved operator), for {@link invalidate}.
|
|
114
|
+
*/
|
|
115
|
+
constructor(ttlMs: number, isAbout?: (value: T, userId: string) => boolean);
|
|
116
|
+
read(resourceKey: readonly string[], session: OxyCallerSession, fresh: boolean, lookup: () => Promise<T>): Promise<T>;
|
|
117
|
+
/** Forget every answer, or those about one person. */
|
|
118
|
+
invalidate(filter?: {
|
|
119
|
+
userId?: string;
|
|
120
|
+
}): void;
|
|
121
|
+
private remember;
|
|
122
|
+
}
|
|
123
|
+
/** A frozen, sorted permission set with `has`. */
|
|
124
|
+
export declare function permissionSet(permissions: readonly string[]): {
|
|
125
|
+
readonly permissions: readonly string[];
|
|
126
|
+
has(permission: string): boolean;
|
|
127
|
+
};
|
|
@@ -17,10 +17,21 @@ export interface CapabilityTicketVerificationOptions {
|
|
|
17
17
|
now?: Date;
|
|
18
18
|
maximumTtlSeconds?: number;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A signing-key lookup that may be asynchronous — `createOxyJwksKeyResolver`
|
|
22
|
+
* is — for the verifiers that already run asynchronously (the middleware and
|
|
23
|
+
* `createLiveCapabilityTicketVerifier`). The ticket's own `kid` is passed; no
|
|
24
|
+
* caller needs to decode the header first.
|
|
25
|
+
*/
|
|
26
|
+
export type CapabilityTicketKeyResolver = (keyId: string) => KeyObject | undefined | Promise<KeyObject | undefined>;
|
|
27
|
+
/** {@link CapabilityTicketVerificationOptions} with a key lookup that may be asynchronous. */
|
|
28
|
+
export interface AsyncCapabilityTicketVerificationOptions extends Omit<CapabilityTicketVerificationOptions, 'resolvePublicKey'> {
|
|
29
|
+
resolvePublicKey: CapabilityTicketKeyResolver;
|
|
30
|
+
}
|
|
20
31
|
export interface CapabilityTicketRequest extends Request {
|
|
21
32
|
capabilityTicket?: CapabilityTicketClaims;
|
|
22
33
|
}
|
|
23
|
-
export interface CapabilityTicketMiddlewareOptions extends
|
|
34
|
+
export interface CapabilityTicketMiddlewareOptions extends AsyncCapabilityTicketVerificationOptions {
|
|
24
35
|
authorize: (claims: CapabilityTicketClaims) => Promise<PolicyDecision>;
|
|
25
36
|
}
|
|
26
37
|
export declare class CapabilityTicketError extends Error {
|
|
@@ -29,6 +40,17 @@ export declare class CapabilityTicketError extends Error {
|
|
|
29
40
|
}
|
|
30
41
|
export declare function issueCapabilityTicket(claims: UnsignedCapabilityTicketClaims, options: CapabilityTicketSigningOptions): string;
|
|
31
42
|
export declare function verifyCapabilityTicket(token: string, options: CapabilityTicketVerificationOptions): CapabilityTicketClaims;
|
|
43
|
+
/**
|
|
44
|
+
* A verifier for one ticket whose key lookup may be asynchronous: the key is
|
|
45
|
+
* resolved once, and the returned `check` re-verifies signature and claims
|
|
46
|
+
* against it (the live verifier checks again after introspection, so a slow
|
|
47
|
+
* authority answer cannot outlive the ticket).
|
|
48
|
+
*
|
|
49
|
+
* @internal Shared by the middleware and `createLiveCapabilityTicketVerifier`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveCapabilityTicketKey(token: string, options: AsyncCapabilityTicketVerificationOptions): Promise<{
|
|
52
|
+
check: () => CapabilityTicketClaims;
|
|
53
|
+
}>;
|
|
32
54
|
export declare function readCapabilityAuthorization(authorization: string | undefined): string | null;
|
|
33
55
|
/**
|
|
34
56
|
* Verifies cryptographic scope first, then asks the authority service for a
|
|
@@ -87,6 +87,45 @@ export declare function normalizeAppOrigins(appOrigins: string[]): Set<string>;
|
|
|
87
87
|
* from `server/index.ts`.
|
|
88
88
|
*/
|
|
89
89
|
export declare function matchesAllowedOrigin(explicit: ReadonlySet<string>, origin: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The origin predicate `createOxyCors` applies, for any other transport that
|
|
92
|
+
* must admit the same browsers: the built-in HTTPS Oxy apex family plus the
|
|
93
|
+
* exact `appOrigins`, never an opaque origin (`exp://`, `file:`, …).
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* const allowed = createOxyOriginCheck(['https://app.example.com']);
|
|
97
|
+
* allowed('https://mention.oxy.so'); // true
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function createOxyOriginCheck(appOrigins?: string[]): (origin: string) => boolean;
|
|
101
|
+
/** Options of {@link createOxySocketCors}. */
|
|
102
|
+
export interface OxySocketCorsOptions {
|
|
103
|
+
/** Explicit additional allowed origins, as {@link OxyCorsOptions.appOrigins}. */
|
|
104
|
+
appOrigins?: string[];
|
|
105
|
+
/** Whether the polling transport sends `Access-Control-Allow-Credentials: true`. Default `true`. */
|
|
106
|
+
allowCredentials?: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The Socket.IO server options that admit exactly the origins `createOxyCors`
|
|
110
|
+
* admits — spread them into `new Server(httpServer, { ...createOxySocketCors(…) })`.
|
|
111
|
+
*
|
|
112
|
+
* Two halves, because Socket.IO has two transports: `cors` answers the
|
|
113
|
+
* long-polling requests (which browsers check), and `allowRequest` refuses a
|
|
114
|
+
* handshake from any other browser origin, which is the only check a WebSocket
|
|
115
|
+
* upgrade gets — browsers apply no CORS to it. A request with no `Origin`
|
|
116
|
+
* header (a native app, a server) is not a browser cross-origin request and is
|
|
117
|
+
* admitted; authenticate it with `oxy.middleware.socket()` as every socket.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createOxySocketCors(options?: OxySocketCorsOptions): {
|
|
120
|
+
cors: {
|
|
121
|
+
origin: (origin: string | undefined, callback: (error: Error | null, allow?: boolean) => void) => void;
|
|
122
|
+
credentials: boolean;
|
|
123
|
+
methods: string[];
|
|
124
|
+
};
|
|
125
|
+
allowRequest: (req: {
|
|
126
|
+
headers: Record<string, string | string[] | undefined>;
|
|
127
|
+
}, callback: (error: string | null | undefined, success: boolean) => void) => void;
|
|
128
|
+
};
|
|
90
129
|
/**
|
|
91
130
|
* Create a strict Oxy CORS middleware. See module docs.
|
|
92
131
|
*
|
|
@@ -4,29 +4,53 @@
|
|
|
4
4
|
* This subpath export provides Express middleware and Node.js-specific
|
|
5
5
|
* utilities that are not available in React Native or browser environments.
|
|
6
6
|
*
|
|
7
|
+
* One `OxyServer` per process, authenticated as the backend's own workload
|
|
8
|
+
* (ADR 0026/0049 — no api key, no client secret):
|
|
9
|
+
*
|
|
7
10
|
* @example
|
|
8
11
|
* ```ts
|
|
9
|
-
* import {
|
|
12
|
+
* import {
|
|
13
|
+
* OxyServer,
|
|
14
|
+
* createOxyAuthMiddleware,
|
|
15
|
+
* createOxyCors,
|
|
16
|
+
* createOxyRateLimit,
|
|
17
|
+
* createOxySecurityHeaders,
|
|
18
|
+
* oxyTrustProxy,
|
|
19
|
+
* } from '@oxy.so/core/server';
|
|
10
20
|
*
|
|
11
|
-
* const oxy = new OxyServer({
|
|
21
|
+
* export const oxy = new OxyServer({
|
|
22
|
+
* baseURL: process.env.OXY_API_URL ?? 'https://api.oxy.so',
|
|
23
|
+
* applicationId: process.env.OXY_APPLICATION_ID,
|
|
24
|
+
* serviceIdentity: 'when-anonymous',
|
|
25
|
+
* });
|
|
12
26
|
*
|
|
27
|
+
* app.set('trust proxy', oxyTrustProxy);
|
|
28
|
+
* app.use(createOxySecurityHeaders(), createOxyCors({ appOrigins }));
|
|
13
29
|
* app.use(createOxyRateLimit(oxy, { store: redisStore }));
|
|
14
30
|
* app.use('/api', createOxyAuthMiddleware(oxy));
|
|
31
|
+
*
|
|
32
|
+
* // As the person behind the request, never on the shared client:
|
|
33
|
+
* const node = await oxy.withUser(req, (user) => user.accounts.get(accountId));
|
|
15
34
|
* ```
|
|
16
35
|
*/
|
|
17
36
|
export { OxyServer, ServiceCredentialMismatchError, ANONYMOUS_SERVICE_TOKEN_RETRY_MS, DEFAULT_SERVICE_TOKEN_AUDIENCE, } from './OxyServer';
|
|
18
37
|
export type { OxyServerConfig, ServiceTokenAudience, ServiceTokenResponse } from './OxyServer';
|
|
38
|
+
export { OxyUserClient } from './userClient';
|
|
39
|
+
export type { OxyUserSource } from './userClient';
|
|
40
|
+
export { OxyAccessError, accessErrorStatus } from './callerAuthority';
|
|
41
|
+
export type { OxyAccessDenyStatus, OxyAccessErrorCode, OxyCallerRequest } from './callerAuthority';
|
|
42
|
+
export type { CallerAccessOptions, OxyAccountAccess, OxyApplicationAccess, } from './callerAccess';
|
|
19
43
|
export { ServerAccountsApi, ServerAppsApi, ServerAgencyApi, ServerAssetsApi, ServerLinkedAccountsApi, ServerNotificationsApi, ServerReputationApi, } from './namespaces';
|
|
20
44
|
export { ServerSafetyApi } from './safety';
|
|
21
45
|
export { ServerSessionApi } from './session';
|
|
22
46
|
export type { ServerSessionValidateOptions } from './session';
|
|
23
47
|
export { createOxyAliaMachineCredentialAuth, getOxyAliaMachineCredentialBearer, isOxyAliaMachinePrincipal, OXY_ALIA_RESOURCE_APPLICATION_ID, OXY_ALIA_MACHINE_SCOPES, } from './aliaMachineCredential';
|
|
24
48
|
export type { AliaMachineCredentialIntrospection, OxyAliaMachinePrincipal, OxyAliaMachineRequest, OxyAliaMachineCredentialHost, } from './aliaMachineCredential';
|
|
25
|
-
export type { CapabilityTicketGrant, CapabilityTicketIntrospection, CreateExecutionAuthorizationInput, ServiceCapabilityCatalog, ServiceUploadBody, ServiceUploadDescription, } from './namespaces';
|
|
49
|
+
export type { CapabilityTicketGrant, CapabilityTicketIntrospection, CreateExecutionAuthorizationInput, ServiceCallOptions, ServiceCapabilityCatalog, ServiceUploadBody, ServiceUploadDescription, } from './namespaces';
|
|
26
50
|
export { OxyAccountEventError, OXY_ACCOUNT_DELETED_EVENT_URI } from './middleware';
|
|
27
51
|
export { APP_PERMISSIONS_CACHE_TTL_MS, OxyAppPermissionError, appPermissionErrorStatus, } from './appPermissions';
|
|
28
52
|
export type { AppPermissionsOptions, OxyAppPermissionDenyStatus, OxyAppPermissionErrorCode, OxyAppPermissions, RequireAppPermissionOptions, } from './appPermissions';
|
|
29
|
-
export type { AuthMiddlewareOptions, OxyAccountEvent, OxyAccountEventFeedItem, OxyAccountEventFeedPage, OxyMiddleware, ServiceActingAsVerification, ServiceApp, VerifyAccountEventOptions, } from './middleware';
|
|
53
|
+
export type { AuthMiddlewareOptions, OxyAccountEvent, OxyAccountEventFeedItem, OxyAccountEventFeedPage, OxyMiddleware, RequireAccessOptions, ServiceActingAsVerification, ServiceApp, VerifyAccountEventOptions, } from './middleware';
|
|
30
54
|
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyActor, getOxyAuthRefusal, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth';
|
|
31
55
|
export type { OxyAuthHost } from './auth';
|
|
32
56
|
export type { OxyAuthenticatedRequest, OxyAuthRefusal, OxyAuthMiddlewareOptions, OxyAuthRequest, OxyBillingPrincipal, OxyRequestAttribution, OxyRequestUser, OxyServiceActingAsContext, OxyServiceAppContext, OxyServiceEnvironment, } from './auth';
|
|
@@ -45,12 +69,12 @@ export { STREAM_KEY_AUTHORIZATION_EVENT_TYPE, StreamKeyAuthorizationVerification
|
|
|
45
69
|
export type { StreamKeyAuthorizationAnswer, StreamKeyAuthorizationRequest, } from './streamKeyAuthorization';
|
|
46
70
|
export { LargeUploadFailedError } from './largeUpload';
|
|
47
71
|
export type { LargeServiceUploadOptions, LargeServiceUploadResult, LargeUploadProgressEvent, LargeUploadSession, LargeUploadSource, } from './largeUpload';
|
|
48
|
-
export { createOxyCors } from './cors';
|
|
49
|
-
export type { OxyCorsOptions } from './cors';
|
|
72
|
+
export { createOxyCors, createOxyOriginCheck, createOxySocketCors } from './cors';
|
|
73
|
+
export type { OxyCorsOptions, OxySocketCorsOptions } from './cors';
|
|
50
74
|
export { buildOxyCspDirectives, buildOxyPagesHeaders, createOxySecurityHeaders, cspSourcesFor, extractInlineScripts, formatOxyCspPolicy, inlineScriptCspHash, OXY_CSP_BASELINE, OXY_FILES_UPLOAD_ORIGIN, } from './securityHeaders';
|
|
51
75
|
export type { OxyCspDirective, OxyCspExtensions, OxyPagesHeadersOptions, OxySecurityHeadersOptions, } from './securityHeaders';
|
|
52
76
|
export { CapabilityTicketError, createCapabilityTicketMiddleware, inputSatisfiesCapabilityLimits, issueCapabilityTicket, readCapabilityAuthorization, verifyCapabilityTicket, } from './capabilityTicket';
|
|
53
|
-
export type { CapabilityTicketMiddlewareOptions, CapabilityTicketRequest, CapabilityTicketSigningOptions, CapabilityTicketVerificationOptions, } from './capabilityTicket';
|
|
77
|
+
export type { AsyncCapabilityTicketVerificationOptions, CapabilityTicketKeyResolver, CapabilityTicketMiddlewareOptions, CapabilityTicketRequest, CapabilityTicketSigningOptions, CapabilityTicketVerificationOptions, } from './capabilityTicket';
|
|
54
78
|
export { createOxyJwksKeyResolver } from './jwks';
|
|
55
79
|
export type { OxyJwksKeyResolverOptions } from './jwks';
|
|
56
80
|
export { OXY_APP_ASSERTION_DEFAULT_ISSUER, OxyAppAssertionError, oxyAppAssertionClaimsSchema, readAppAssertionHeader, readOxyAppAssertionKeyId, signOxyAppAssertion, verifyAppAssertion, verifyOxyAppAssertion, } from './appAssertion';
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type CapabilityTicketClaims } from '@oxy.so/contracts';
|
|
2
|
-
import { type
|
|
2
|
+
import { type AsyncCapabilityTicketVerificationOptions } from './capabilityTicket';
|
|
3
3
|
import type { CapabilityTicketIntrospection } from './namespaces';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `resolvePublicKey` may be asynchronous — pass `createOxyJwksKeyResolver({ jwksUrl })`
|
|
6
|
+
* as it is; the ticket's `kid` is looked up once per verification.
|
|
7
|
+
*/
|
|
8
|
+
export interface LiveCapabilityTicketVerifierOptions extends AsyncCapabilityTicketVerificationOptions {
|
|
5
9
|
issuer: string;
|
|
6
10
|
introspect: (ticket: string, options: {
|
|
7
11
|
signal: AbortSignal;
|