@oxy.so/core 7.16.0 → 7.18.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.
Files changed (62) hide show
  1. package/README.md +24 -13
  2. package/dist/cjs/server/OxyServer.js +85 -3
  3. package/dist/cjs/server/appPermissions.js +14 -92
  4. package/dist/cjs/server/callerAccess.js +170 -0
  5. package/dist/cjs/server/callerAuthority.js +206 -0
  6. package/dist/cjs/server/capabilityTicket.js +39 -6
  7. package/dist/cjs/server/cors.js +41 -2
  8. package/dist/cjs/server/index.js +33 -5
  9. package/dist/cjs/server/liveCapabilityTicket.js +4 -2
  10. package/dist/cjs/server/middleware.js +104 -2
  11. package/dist/cjs/server/namespaces.js +15 -4
  12. package/dist/cjs/server/safeFetch.js +29 -1
  13. package/dist/cjs/server/userClient.js +127 -0
  14. package/dist/cjs/utils/cache.js +58 -22
  15. package/dist/esm/server/OxyServer.js +85 -3
  16. package/dist/esm/server/appPermissions.js +12 -89
  17. package/dist/esm/server/callerAccess.js +164 -0
  18. package/dist/esm/server/callerAuthority.js +194 -0
  19. package/dist/esm/server/capabilityTicket.js +38 -6
  20. package/dist/esm/server/cors.js +39 -2
  21. package/dist/esm/server/index.js +26 -3
  22. package/dist/esm/server/liveCapabilityTicket.js +5 -3
  23. package/dist/esm/server/middleware.js +104 -2
  24. package/dist/esm/server/namespaces.js +15 -4
  25. package/dist/esm/server/safeFetch.js +29 -1
  26. package/dist/esm/server/userClient.js +121 -0
  27. package/dist/esm/utils/cache.js +57 -22
  28. package/dist/types/api/accounts.d.ts +8 -1
  29. package/dist/types/api/apps.d.ts +22 -4
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/server/OxyServer.d.ts +74 -7
  32. package/dist/types/server/appPermissions.d.ts +3 -13
  33. package/dist/types/server/auth.d.ts +5 -0
  34. package/dist/types/server/callerAccess.d.ts +99 -0
  35. package/dist/types/server/callerAuthority.d.ts +127 -0
  36. package/dist/types/server/capabilityTicket.d.ts +23 -1
  37. package/dist/types/server/cors.d.ts +39 -0
  38. package/dist/types/server/index.d.ts +31 -7
  39. package/dist/types/server/liveCapabilityTicket.d.ts +6 -2
  40. package/dist/types/server/middleware.d.ts +34 -2
  41. package/dist/types/server/namespaces.d.ts +18 -2
  42. package/dist/types/server/safeFetch.d.ts +10 -0
  43. package/dist/types/server/userClient.d.ts +71 -0
  44. package/dist/types/utils/cache.d.ts +13 -0
  45. package/package.json +1 -1
  46. package/src/api/accounts.ts +8 -1
  47. package/src/api/apps.ts +23 -4
  48. package/src/index.ts +1 -0
  49. package/src/server/OxyServer.ts +126 -7
  50. package/src/server/appPermissions.ts +28 -105
  51. package/src/server/auth.ts +5 -0
  52. package/src/server/callerAccess.ts +264 -0
  53. package/src/server/callerAuthority.ts +267 -0
  54. package/src/server/capabilityTicket.ts +76 -17
  55. package/src/server/cors.ts +63 -2
  56. package/src/server/index.ts +38 -4
  57. package/src/server/liveCapabilityTicket.ts +12 -5
  58. package/src/server/middleware.ts +171 -4
  59. package/src/server/namespaces.ts +36 -5
  60. package/src/server/safeFetch.ts +31 -1
  61. package/src/server/userClient.ts +147 -0
  62. 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 application-permissions answer is reused, per session
51
- * (default 30 000 ms). This is the longest a revoked grant keeps working on
52
- * this backend for a read that does not pass `fresh: true`. `0` disables the
53
- * cache.
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 application-permission answers — all of them, or one
310
- * person's (as the session user or the resolved operator). For a backend
311
- * that learns of a revocation out of band and will not wait out the TTL.
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 function appPermissionErrorBody(status: number): {
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,99 @@
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 { ApplicationStatus } from '../api/apps';
32
+ import { type OxyCallerRequest } from './callerAuthority';
33
+ /** What the person behind a request may do in one Oxy account. */
34
+ export interface OxyAccountAccess {
35
+ readonly accountId: string;
36
+ readonly kind: AccountKind;
37
+ /** `self` for their own personal account, `owner`, or `member` (direct or inherited). */
38
+ readonly relationship: AccountRelationship;
39
+ /** Informational; gate on `permissions` / `has`, never on the role name. */
40
+ readonly role: string;
41
+ /** Where the access comes from: `self`, a `direct` membership, or one `inherited` from an ancestor. */
42
+ readonly source: AccountAccessSource;
43
+ /** The EFFECTIVE account permissions (role baseline, grants, revokes), sorted. */
44
+ readonly permissions: readonly string[];
45
+ has(permission: string): boolean;
46
+ }
47
+ /** What the person behind a request may do to one Oxy application, through its owning account. */
48
+ export interface OxyApplicationAccess {
49
+ readonly applicationId: string;
50
+ /** The account that owns the application; the access derives from the caller's membership there. */
51
+ readonly ownerAccountId: string;
52
+ /**
53
+ * Whether the application is one of Oxy's own: `isOfficial`, `isInternal`,
54
+ * or typed `first_party` / `internal` / `system` — the rule oxy-api applies
55
+ * to every trust decision (`isTrustedApplication`). All of those fields are
56
+ * staff-controlled; a self-service `third_party` application is never
57
+ * official, whatever its owner sets.
58
+ */
59
+ readonly official: boolean;
60
+ /**
61
+ * The application's lifecycle status. A `suspended` or `pending_review`
62
+ * application is still answered (its members may read it); whether it may
63
+ * act is the caller's decision.
64
+ */
65
+ readonly status: ApplicationStatus;
66
+ /** Informational; gate on `permissions` / `has`, never on the role name. */
67
+ readonly role: string;
68
+ /** The application permissions (`app:read`, `app:update`, `webhooks:update`, …), sorted. */
69
+ readonly permissions: readonly string[];
70
+ has(permission: string): boolean;
71
+ }
72
+ export interface CallerAccessOptions {
73
+ /** Skip the cache and ask Oxy (the answer still refreshes the cache). Use before a write. */
74
+ fresh?: boolean;
75
+ }
76
+ /** What the readers need from the server they belong to. */
77
+ export interface CallerAccessHost {
78
+ /** `GET /accounts/:id` with this bearer, uncached. Rejects with the API error. */
79
+ readAccount(accountId: string, accessToken: string): Promise<unknown>;
80
+ /** `GET /applications/:id` with this bearer, uncached. Rejects with the API error. */
81
+ readApplication(applicationId: string, accessToken: string): Promise<unknown>;
82
+ readonly ttlMs: number;
83
+ }
84
+ /** `GET /accounts/:id` → the caller's effective access, or `null` for a malformed answer. */
85
+ export declare function parseAccountAccess(accountId: string, answer: unknown): OxyAccountAccess | null;
86
+ /** `GET /applications/:id` → the caller's access over it, or `null` for a malformed answer. */
87
+ export declare function parseApplicationAccess(applicationId: string, answer: unknown): OxyApplicationAccess | null;
88
+ /** The caches and lookups behind `accountAccessFor` / `applicationAccessFor`. One per server. */
89
+ export declare class OxyCallerAccessResolver {
90
+ private readonly host;
91
+ private readonly accounts;
92
+ private readonly applications;
93
+ constructor(host: CallerAccessHost);
94
+ account(req: OxyCallerRequest, accountId: string, options?: CallerAccessOptions): Promise<OxyAccountAccess>;
95
+ application(req: OxyCallerRequest, applicationId: string, options?: CallerAccessOptions): Promise<OxyApplicationAccess>;
96
+ invalidate(filter?: {
97
+ userId?: string;
98
+ }): void;
99
+ }
@@ -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 CapabilityTicketVerificationOptions {
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 { OxyServer, createOxyAuthMiddleware, createOxyRateLimit } from '@oxy.so/core/server';
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({ baseURL: 'https://api.oxy.so', serviceAuth: { apiKey, apiSecret } });
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 CapabilityTicketVerificationOptions } from './capabilityTicket';
2
+ import { type AsyncCapabilityTicketVerificationOptions } from './capabilityTicket';
3
3
  import type { CapabilityTicketIntrospection } from './namespaces';
4
- export interface LiveCapabilityTicketVerifierOptions extends CapabilityTicketVerificationOptions {
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;