@oxyhq/core 1.11.13 → 1.11.14

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.
@@ -112,13 +112,23 @@ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxySe
112
112
  signUpWithRedirect(options?: RedirectAuthOptions): void;
113
113
  auth(options?: {
114
114
  debug?: boolean;
115
- onError?: (error: any) => any;
115
+ onError?: (error: unknown) => unknown;
116
116
  loadUser?: boolean;
117
117
  optional?: boolean;
118
- }): (req: any, res: any, next: any) => Promise<void>;
118
+ jwtSecret?: string;
119
+ expectedIssuer?: string;
120
+ expectedAudience?: string;
121
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
119
122
  authSocket(options?: {
120
123
  debug?: boolean;
121
- }): (socket: any, next: (err?: Error) => void) => Promise<void>;
124
+ }): (socket: unknown, next: (err?: Error) => void) => Promise<void>;
125
+ serviceAuth(options?: {
126
+ debug?: boolean;
127
+ jwtSecret?: string;
128
+ expectedIssuer?: string;
129
+ expectedAudience?: string;
130
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
131
+ requireScope(scope: string): (req: unknown, res: unknown, next: (err?: unknown) => void) => void;
122
132
  assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<unknown>;
123
133
  }
124
134
  export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
@@ -129,7 +139,7 @@ export declare const OXY_CLOUD_URL = "https://cloud.oxy.so";
129
139
  /**
130
140
  * Export the default Oxy API URL (for documentation)
131
141
  */
132
- export declare const OXY_API_URL: string;
142
+ export declare const OXY_API_URL: any;
133
143
  /**
134
144
  * Pre-configured client instance for easy import
135
145
  * Uses OXY_API_URL as baseURL and OXY_CLOUD_URL as cloudURL
@@ -23,8 +23,9 @@ export type { CrossDomainAuthOptions } from './CrossDomainAuth';
23
23
  export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
24
24
  export type { PopupAuthOptions } from './mixins/OxyServices.popup';
25
25
  export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
26
+ export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
26
27
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
27
- export type { ServiceApp } from './mixins/OxyServices.utility';
28
+ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
28
29
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount } from './mixins/OxyServices.managedAccounts';
29
30
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse } from './mixins/OxyServices.contacts';
30
31
  export { KeyManager, SignatureService, RecoveryPhraseService, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto';
@@ -35,7 +36,7 @@ export type { TopicData, TopicTranslation } from './models/Topic';
35
36
  export { TopicType, TopicSource } from './models/Topic';
36
37
  export { DeviceManager } from './utils/deviceManager';
37
38
  export type { DeviceFingerprint, StoredDeviceInfo } from './utils/deviceManager';
38
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, } from './utils/languageUtils';
39
+ export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils';
39
40
  export type { LanguageMetadata } from './utils/languageUtils';
40
41
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform';
41
42
  export type { PlatformOS } from './utils/platform';
@@ -34,33 +34,93 @@ export interface ServiceTokenResponse {
34
34
  expiresIn: number;
35
35
  appName: string;
36
36
  }
37
+ /**
38
+ * One cache entry per (apiKey hash) → issued token + the secret that produced it.
39
+ * The secret is kept around in raw Buffer form so we can perform a
40
+ * constant-time compare against any reused credential pair — this prevents an
41
+ * attacker who learned a victim's apiKey from receiving the victim's cached
42
+ * service token by simply guessing the secret.
43
+ *
44
+ * @internal
45
+ */
46
+ interface ServiceTokenCacheEntry {
47
+ token: string;
48
+ /** Expiry as ms since epoch */
49
+ expiresAt: number;
50
+ /** Raw secret stored as Buffer for constant-time comparison on cache hit */
51
+ secretBuf: Buffer;
52
+ /** In-flight refresh promise (deduplicates concurrent callers) */
53
+ pending: Promise<string> | null;
54
+ }
55
+ /**
56
+ * Sentinel error raised when getServiceToken() is called with a known apiKey
57
+ * but a non-matching secret. Indicates either credential drift in the caller
58
+ * or a cross-tenant cache lookup attempt. Surface as a 401-equivalent.
59
+ */
60
+ export declare class ServiceCredentialMismatchError extends Error {
61
+ constructor();
62
+ }
37
63
  export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T): {
38
64
  new (...args: any[]): {
39
- /** @internal */ _serviceToken: string | null;
40
- /** @internal */ _serviceTokenExp: number;
41
- /** @internal */ _serviceApiKey: string | null;
42
- /** @internal */ _serviceApiSecret: string | null;
43
65
  /**
44
- * In-flight promise for service token fetch. Used to deduplicate concurrent
45
- * calls to getServiceToken() — pattern mirrors AuthManager.refreshToken().
66
+ * Per-credential token cache.
67
+ *
68
+ * Keyed by SHA-256(apiKey). Each entry carries:
69
+ * - the issued service JWT
70
+ * - its expiry timestamp
71
+ * - the secret that produced it (Buffer for constant-time compare)
72
+ * - an optional in-flight promise to deduplicate concurrent refreshes
73
+ *
74
+ * The previous implementation kept ONE token/exp pair per OxyServices
75
+ * instance. That meant calling `getServiceToken(keyA, secretA)` populated
76
+ * the cache, and a subsequent `getServiceToken(keyB, secretB)` (different
77
+ * tenant) would receive tenant A's token. This is fixed by routing every
78
+ * lookup through the Map.
79
+ *
80
+ * @internal
81
+ */
82
+ _serviceTokenCache: Map<string, ServiceTokenCacheEntry>;
83
+ /** @internal Raw apiKey stored by configureServiceAuth() for use by getServiceToken() */
84
+ _serviceApiKey: string | null;
85
+ /** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
86
+ _serviceApiSecret: string | null;
87
+ /**
88
+ * Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
89
+ * tokens are only ever issued by a Node host (the SDK on web/RN never has
90
+ * the apiSecret in the first place), so we can rely on Node crypto here.
91
+ *
46
92
  * @internal
47
93
  */
48
- _serviceTokenPromise: Promise<string> | null;
94
+ _hashApiKey(apiKey: string): Promise<string>;
49
95
  /**
50
96
  * Configure service credentials for internal service-to-service communication.
51
97
  * Call this once at startup so that getServiceToken() and makeServiceRequest()
52
98
  * can automatically obtain and refresh tokens.
53
99
  *
100
+ * Calling this with credentials that differ from a previously-configured pair
101
+ * is allowed — each `(apiKey, apiSecret)` pair is cached independently, so
102
+ * legitimate multi-tenant hosts that need to switch credentials cannot leak
103
+ * one tenant's token to another tenant on the same instance.
104
+ *
54
105
  * @param apiKey - DeveloperApp API key (oxy_dk_*)
55
106
  * @param apiSecret - DeveloperApp API secret
56
107
  */
57
108
  configureServiceAuth(apiKey: string, apiSecret: string): void;
58
109
  /**
59
110
  * Get a service token for internal service-to-service communication.
60
- * Tokens are short-lived (1h) and automatically cached/refreshed.
111
+ * Tokens are short-lived (1h) and automatically cached/refreshed per
112
+ * `(apiKey, apiSecret)` pair.
113
+ *
114
+ * Concurrent callers for the same credential pair share a single in-flight
115
+ * request to avoid hammering `/auth/service-token` when the cache is empty
116
+ * or expired.
61
117
  *
62
- * Concurrent callers share a single in-flight request to avoid hammering
63
- * `/auth/service-token` when the cache is empty or expired.
118
+ * **Security guarantee:** if the cache already holds a token for this
119
+ * apiKey but the supplied apiSecret does not constant-time match the
120
+ * secret that originally produced that token, this method throws
121
+ * `ServiceCredentialMismatchError` instead of returning the cached token.
122
+ * This prevents an attacker who learned a peer's apiKey from extracting
123
+ * their service token by polling with a wrong secret.
64
124
  *
65
125
  * @param apiKey - DeveloperApp API key (optional if configureServiceAuth was called)
66
126
  * @param apiSecret - DeveloperApp API secret (optional if configureServiceAuth was called)
@@ -71,7 +131,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
71
131
  * Separated so getServiceToken() can deduplicate concurrent calls.
72
132
  * @internal
73
133
  */
74
- _doFetchServiceToken(key: string, secret: string): Promise<string>;
134
+ _doFetchServiceToken(key: string, secret: string, cacheKey: string, secretBuf: Buffer): Promise<string>;
75
135
  /**
76
136
  * Make an authenticated request on behalf of a user using a service token.
77
137
  * Automatically obtains/refreshes the service token.
@@ -240,3 +300,4 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
240
300
  }>;
241
301
  };
242
302
  } & T;
303
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ApiError } from '../models/interfaces';
1
+ import type { ApiError, User } from '../models/interfaces';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
3
  /**
4
4
  * Result from the managed-accounts verification endpoint.
@@ -9,11 +9,31 @@ interface ActingAsVerification {
9
9
  role: 'owner' | 'admin' | 'editor';
10
10
  }
11
11
  /**
12
- * Service app metadata attached to requests authenticated with service tokens
12
+ * Result from the service-acting-as verification endpoint.
13
+ * Confirms that a given service app holds an active delegation grant for
14
+ * the supplied user, along with the explicit scope list the grant covers.
15
+ *
16
+ * The api side persists these via the `ServiceActingAs` model:
17
+ * { serviceAppId, userId, scopes: string[], grantedAt, expiresAt }
18
+ *
19
+ * The SDK never inspects the grant directly — it round-trips through
20
+ * `GET /internal/service-acting-as/verify?appId=...&userId=...` so the
21
+ * authoritative store stays server-side.
22
+ */
23
+ export interface ServiceActingAsVerification {
24
+ authorized: boolean;
25
+ scopes: string[];
26
+ }
27
+ /**
28
+ * Service app metadata attached to requests authenticated with service tokens.
29
+ * `scopes` reflects the scopes granted to the app at signup time (from the
30
+ * `DeveloperApp.scopes` field); route-level checks can require additional
31
+ * scope-narrowing via `requireScope()`.
13
32
  */
14
33
  export interface ServiceApp {
15
34
  appId: string;
16
35
  appName: string;
36
+ scopes: string[];
17
37
  }
18
38
  /**
19
39
  * Options for oxyClient.auth() middleware
@@ -22,7 +42,7 @@ interface AuthMiddlewareOptions {
22
42
  /** Enable debug logging (default: false) */
23
43
  debug?: boolean;
24
44
  /** Custom error handler - receives error object, can return response */
25
- onError?: (error: ApiError) => any;
45
+ onError?: (error: ApiError) => unknown;
26
46
  /** Load full user profile from API (default: false for performance) */
27
47
  loadUser?: boolean;
28
48
  /** Optional auth - attach user if token present but don't block (default: false) */
@@ -31,8 +51,24 @@ interface AuthMiddlewareOptions {
31
51
  * JWT secret for verifying service token signatures locally.
32
52
  * When provided, service tokens will be cryptographically verified.
33
53
  * When omitted, service tokens will be rejected (secure default).
54
+ *
55
+ * **Migration note (>=1.11.14):** the Oxy API now signs service tokens
56
+ * with a dedicated `SERVICE_TOKEN_SECRET` distinct from `ACCESS_TOKEN_SECRET`.
57
+ * Pass that value here. If you keep passing the access-token secret you will
58
+ * still verify ALL signed-by-Oxy tokens (which is the whole class of bug
59
+ * H4 was supposed to prevent — DO NOT do that in production).
34
60
  */
35
61
  jwtSecret?: string;
62
+ /**
63
+ * Expected JWT issuer. Defaults to `'oxy-auth'`. Override only if you run
64
+ * a private fork of the Oxy auth server under a different `iss` claim.
65
+ */
66
+ expectedIssuer?: string;
67
+ /**
68
+ * Expected JWT audience. Defaults to `'oxy-api'`. Override only if your
69
+ * private fork mints tokens for a different audience.
70
+ */
71
+ expectedAudience?: string;
36
72
  }
37
73
  export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T): {
38
74
  new (...args: any[]): {
@@ -41,6 +77,17 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
41
77
  result: ActingAsVerification | null;
42
78
  expiresAt: number;
43
79
  }>;
80
+ /**
81
+ * In-memory cache for service-acting-as verification.
82
+ * Negative results are cached for 1min to avoid hammering the verify
83
+ * endpoint when a service is misconfigured; positive grants are cached
84
+ * for 5min to amortize the round-trip without holding stale grants too long.
85
+ * @internal
86
+ */
87
+ _serviceActingAsCache: Map<string, {
88
+ result: ServiceActingAsVerification | null;
89
+ expiresAt: number;
90
+ }>;
44
91
  /**
45
92
  * Verify that a user is authorized to act as a managed account.
46
93
  * Results are cached in-memory for 5 minutes to avoid repeated API calls.
@@ -48,6 +95,19 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
48
95
  * @internal Used by the auth() middleware — not part of the public API
49
96
  */
50
97
  verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null>;
98
+ /**
99
+ * Verify that a service app holds an active delegation grant authorising
100
+ * it to act on behalf of `userId`. Returns the grant (with allowed scopes)
101
+ * on success or `null` if no valid grant exists. Negative answers are
102
+ * cached briefly to protect the verify endpoint from misconfigured callers.
103
+ *
104
+ * Implemented as a per-instance Map keyed by `appId:userId`. Cached
105
+ * positive grants live for 5 minutes (acceptable staleness window for an
106
+ * impersonation grant); revocations propagate within that window.
107
+ *
108
+ * @internal Used by the auth() middleware — not part of the public API
109
+ */
110
+ verifyServiceActingAs(appId: string, userId: string): Promise<ServiceActingAsVerification | null>;
51
111
  /**
52
112
  * Fetch link metadata
53
113
  */
@@ -70,9 +130,17 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
70
130
  * - Security comes from API-based session validation (`validateSession()`)
71
131
  * which checks the session server-side on every request
72
132
  * - Service tokens (type: 'service') DO use cryptographic HMAC verification
73
- * via the `jwtSecret` option, since they are stateless
133
+ * via the `jwtSecret` option, since they are stateless. Service tokens
134
+ * are additionally checked for `aud`, `iss`, and `type` claims to prevent
135
+ * cross-token-type confusion attacks.
74
136
  * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
75
- * direct access to `ACCESS_TOKEN_SECRET`
137
+ * direct access to `SERVICE_TOKEN_SECRET` / `ACCESS_TOKEN_SECRET`.
138
+ *
139
+ * **Service-token delegation (X-Oxy-User-Id):**
140
+ * When a service token is accompanied by `X-Oxy-User-Id`, the SDK calls
141
+ * `verifyServiceActingAs(appId, userId)` to confirm an explicit delegation
142
+ * grant exists before attaching `req.userId`. A missing/expired grant
143
+ * results in a 403 — there is no fail-open path.
76
144
  *
77
145
  * @example
78
146
  * ```typescript
@@ -81,7 +149,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
81
149
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
82
150
  *
83
151
  * // Protect all routes under /protected
84
- * app.use('/protected', oxy.auth());
152
+ * app.use('/protected', oxy.auth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
85
153
  *
86
154
  * // Access user in route handler
87
155
  * app.get('/protected/me', (req, res) => {
@@ -93,12 +161,15 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
93
161
  *
94
162
  * // Optional auth - attach user if present, don't block if absent
95
163
  * app.use('/public', oxy.auth({ optional: true }));
164
+ *
165
+ * // Require a specific scope on a service-token-protected route
166
+ * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }), oxy.requireScope('files:write'));
96
167
  * ```
97
168
  *
98
169
  * @param options Optional configuration
99
170
  * @returns Express middleware function
100
171
  */
101
- auth(options?: AuthMiddlewareOptions): (req: any, res: any, next: any) => Promise<any>;
172
+ auth(options?: AuthMiddlewareOptions): (req: AuthReq, res: AuthRes, next: AuthNext) => Promise<unknown>;
102
173
  /**
103
174
  * Socket.IO authentication middleware factory
104
175
  *
@@ -125,7 +196,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
125
196
  */
126
197
  authSocket(options?: {
127
198
  debug?: boolean;
128
- }): (socket: any, next: (err?: Error) => void) => Promise<void>;
199
+ }): (socket: SocketLike, next: (err?: Error) => void) => Promise<void>;
129
200
  /**
130
201
  * Express.js middleware that only allows service tokens.
131
202
  * Use this for internal-only endpoints that should not be accessible
@@ -134,7 +205,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
134
205
  * @example
135
206
  * ```typescript
136
207
  * // Protect internal endpoints
137
- * app.use('/internal', oxy.serviceAuth());
208
+ * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
138
209
  *
139
210
  * app.post('/internal/trigger', (req, res) => {
140
211
  * console.log('Service app:', req.serviceApp);
@@ -145,7 +216,30 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
145
216
  serviceAuth(options?: {
146
217
  debug?: boolean;
147
218
  jwtSecret?: string;
148
- }): (req: any, res: any, next: any) => Promise<void>;
219
+ expectedIssuer?: string;
220
+ expectedAudience?: string;
221
+ }): (req: AuthReq, res: AuthRes, next: AuthNext) => Promise<void>;
222
+ /**
223
+ * Express.js middleware that enforces a specific service-token scope.
224
+ *
225
+ * Mount AFTER `auth()` / `serviceAuth()` — relies on `req.serviceApp` and
226
+ * (when delegation is in effect) `req.serviceActingAs.scopes`. The scope
227
+ * is granted if EITHER list contains it, mirroring the OAuth2 model where
228
+ * the app's app-level scopes and the per-user delegated scopes both count.
229
+ *
230
+ * Requests authenticated as a regular user (no service token) are rejected
231
+ * with 403 — scope-protected endpoints are service-to-service by design.
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * app.use(
236
+ * '/internal/files',
237
+ * oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }),
238
+ * oxy.requireScope('files:write'),
239
+ * );
240
+ * ```
241
+ */
242
+ requireScope(scope: string): (req: AuthReq, res: AuthRes, next: AuthNext) => void;
149
243
  httpService: import("../HttpService").HttpService;
150
244
  cloudURL: string;
151
245
  config: import("../OxyServices.base").OxyConfig;
@@ -196,4 +290,44 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
196
290
  }>;
197
291
  };
198
292
  } & T;
293
+ interface AuthReq {
294
+ method?: string;
295
+ path?: string;
296
+ headers: Record<string, string | string[] | undefined>;
297
+ query?: Record<string, unknown>;
298
+ userId?: string | null;
299
+ user?: User | null;
300
+ accessToken?: string;
301
+ sessionId?: string | null;
302
+ serviceApp?: ServiceApp;
303
+ serviceActingAs?: {
304
+ userId: string;
305
+ scopes: string[];
306
+ };
307
+ actingAs?: {
308
+ userId: string;
309
+ role: string;
310
+ };
311
+ originalUser?: {
312
+ id: string;
313
+ } & Partial<User>;
314
+ }
315
+ interface AuthRes {
316
+ status(code: number): AuthRes;
317
+ json(body: unknown): unknown;
318
+ }
319
+ type AuthNext = (err?: unknown) => void;
320
+ interface SocketLike {
321
+ handshake?: {
322
+ auth?: {
323
+ token?: string;
324
+ };
325
+ };
326
+ data?: Record<string, unknown>;
327
+ user?: {
328
+ id: string;
329
+ userId: string;
330
+ sessionId?: string | null;
331
+ };
332
+ }
199
333
  export {};
@@ -35,3 +35,4 @@ export declare function getNativeLanguageName(languageCode: string | null | unde
35
35
  * @returns Normalized BCP-47 language code
36
36
  */
37
37
  export declare function normalizeLanguageCode(lang?: string | null): string;
38
+ export declare function isRTLLocale(locale?: string | null): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.11.13",
3
+ "version": "1.11.14",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -70,7 +70,22 @@
70
70
  "clean": "rm -rf dist",
71
71
  "typescript": "tsc --noEmit",
72
72
  "test": "jest --passWithNoTests",
73
- "lint": "biome lint --error-on-warnings ./src"
73
+ "lint": "biome lint --error-on-warnings ./src",
74
+ "release": "rm -rf dist && bun run build && release-it"
75
+ },
76
+ "release-it": {
77
+ "git": {
78
+ "tagName": "@oxyhq/core@${version}",
79
+ "tagAnnotation": "Release @oxyhq/core@${version}",
80
+ "commitMessage": "chore(core): release @oxyhq/core@${version}"
81
+ },
82
+ "github": {
83
+ "release": true,
84
+ "releaseName": "@oxyhq/core@${version}"
85
+ },
86
+ "npm": {
87
+ "publish": true
88
+ }
74
89
  },
75
90
  "dependencies": {
76
91
  "bip39": "^3.1.0",
@@ -105,6 +120,7 @@
105
120
  "@types/node": "^20.19.9",
106
121
  "expo-crypto": "~56.0.3",
107
122
  "expo-secure-store": "~56.0.4",
123
+ "release-it": "^19.0.6",
108
124
  "typescript": "^5.9.2"
109
125
  }
110
126
  }
@@ -137,15 +137,29 @@ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxySe
137
137
  // Express.js middleware
138
138
  auth(options?: {
139
139
  debug?: boolean;
140
- onError?: (error: any) => any;
140
+ onError?: (error: unknown) => unknown;
141
141
  loadUser?: boolean;
142
142
  optional?: boolean;
143
- }): (req: any, res: any, next: any) => Promise<void>;
143
+ jwtSecret?: string;
144
+ expectedIssuer?: string;
145
+ expectedAudience?: string;
146
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
144
147
 
145
148
  // Socket.IO middleware
146
149
  authSocket(options?: {
147
150
  debug?: boolean;
148
- }): (socket: any, next: (err?: Error) => void) => Promise<void>;
151
+ }): (socket: unknown, next: (err?: Error) => void) => Promise<void>;
152
+
153
+ // Service-token-only middleware (delegates to auth() internally)
154
+ serviceAuth(options?: {
155
+ debug?: boolean;
156
+ jwtSecret?: string;
157
+ expectedIssuer?: string;
158
+ expectedAudience?: string;
159
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
160
+
161
+ // Scope enforcement for service-token-protected routes
162
+ requireScope(scope: string): (req: unknown, res: unknown, next: (err?: unknown) => void) => void;
149
163
 
150
164
  // Asset management
151
165
  assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<unknown>;
package/src/index.ts CHANGED
@@ -30,8 +30,9 @@ export type { CrossDomainAuthOptions } from './CrossDomainAuth';
30
30
  export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
31
31
  export type { PopupAuthOptions } from './mixins/OxyServices.popup';
32
32
  export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
33
+ export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
33
34
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
34
- export type { ServiceApp } from './mixins/OxyServices.utility';
35
+ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
35
36
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount } from './mixins/OxyServices.managedAccounts';
36
37
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse } from './mixins/OxyServices.contacts';
37
38
 
@@ -62,6 +63,7 @@ export {
62
63
  getLanguageName,
63
64
  getNativeLanguageName,
64
65
  normalizeLanguageCode,
66
+ isRTLLocale,
65
67
  } from './utils/languageUtils';
66
68
  export type { LanguageMetadata } from './utils/languageUtils';
67
69