@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/mixins/OxyServices.auth.js +133 -27
- package/dist/cjs/mixins/OxyServices.utility.js +405 -75
- package/dist/cjs/utils/languageUtils.js +22 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.auth.js +131 -27
- package/dist/esm/mixins/OxyServices.utility.js +405 -75
- package/dist/esm/utils/languageUtils.js +21 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.d.ts +14 -4
- package/dist/types/index.d.ts +3 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +72 -11
- package/dist/types/mixins/OxyServices.utility.d.ts +144 -10
- package/dist/types/utils/languageUtils.d.ts +1 -0
- package/package.json +18 -2
- package/src/OxyServices.ts +17 -3
- package/src/index.ts +3 -1
- package/src/mixins/OxyServices.auth.ts +160 -28
- package/src/mixins/OxyServices.utility.ts +551 -87
- package/src/mixins/__tests__/serviceAuth.test.ts +623 -0
- package/src/utils/languageUtils.ts +23 -2
|
@@ -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:
|
|
115
|
+
onError?: (error: unknown) => unknown;
|
|
116
116
|
loadUser?: boolean;
|
|
117
117
|
optional?: boolean;
|
|
118
|
-
|
|
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:
|
|
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:
|
|
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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
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) =>
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/src/OxyServices.ts
CHANGED
|
@@ -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:
|
|
140
|
+
onError?: (error: unknown) => unknown;
|
|
141
141
|
loadUser?: boolean;
|
|
142
142
|
optional?: boolean;
|
|
143
|
-
|
|
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:
|
|
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
|
|