@oxyhq/core 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +6 -18
- package/dist/cjs/OxyServices.base.js +0 -21
- package/dist/cjs/crypto/keyManager.js +7 -7
- package/dist/cjs/crypto/polyfill.js +6 -5
- package/dist/cjs/crypto/signatureService.js +44 -220
- package/dist/cjs/i18n/locales/en-US.json +391 -17
- package/dist/cjs/i18n/locales/es-ES.json +391 -17
- package/dist/cjs/i18n/locales/locales/en-US.json +391 -17
- package/dist/cjs/i18n/locales/locales/es-ES.json +391 -17
- package/dist/cjs/index.js +4 -8
- package/dist/cjs/mixins/OxyServices.accounts.js +54 -0
- package/dist/cjs/mixins/OxyServices.assets.js +2 -2
- package/dist/cjs/mixins/OxyServices.auth.js +3 -3
- package/dist/cjs/mixins/OxyServices.civic.js +3 -3
- package/dist/cjs/mixins/OxyServices.language.js +2 -2
- package/dist/cjs/mixins/OxyServices.utility.js +7 -95
- package/dist/cjs/utils/cacheKey.js +17 -19
- package/dist/cjs/utils/deviceManager.js +2 -2
- package/dist/cjs/utils/platform.js +0 -14
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -18
- package/dist/esm/OxyServices.base.js +0 -21
- package/dist/esm/crypto/keyManager.js +4 -4
- package/dist/esm/crypto/polyfill.js +5 -4
- package/dist/esm/crypto/signatureService.js +39 -214
- package/dist/esm/i18n/locales/en-US.json +391 -17
- package/dist/esm/i18n/locales/es-ES.json +391 -17
- package/dist/esm/i18n/locales/locales/en-US.json +391 -17
- package/dist/esm/i18n/locales/locales/es-ES.json +391 -17
- package/dist/esm/index.js +1 -2
- package/dist/esm/mixins/OxyServices.accounts.js +54 -0
- package/dist/esm/mixins/OxyServices.assets.js +1 -1
- package/dist/esm/mixins/OxyServices.auth.js +1 -1
- package/dist/esm/mixins/OxyServices.civic.js +3 -3
- package/dist/esm/mixins/OxyServices.language.js +1 -1
- package/dist/esm/mixins/OxyServices.utility.js +6 -94
- package/dist/esm/utils/cacheKey.js +17 -19
- package/dist/esm/utils/deviceManager.js +1 -1
- package/dist/esm/utils/platform.js +0 -12
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +3 -6
- package/dist/types/OxyServices.base.d.ts +0 -17
- package/dist/types/crypto/polyfill.d.ts +2 -2
- package/dist/types/crypto/signatureService.d.ts +18 -84
- package/dist/types/index.d.ts +3 -4
- package/dist/types/mixins/OxyServices.accounts.d.ts +57 -5
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +0 -2
- package/dist/types/mixins/OxyServices.civic.d.ts +3 -5
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +8 -5
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -44
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.redirect.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -2
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -32
- package/dist/types/server/auth.d.ts +0 -6
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/utils/cacheKey.d.ts +6 -7
- package/dist/types/utils/platform.d.ts +0 -8
- package/package.json +4 -7
- package/src/HttpService.ts +6 -22
- package/src/OxyServices.base.ts +0 -23
- package/src/__tests__/httpServiceCache.test.ts +0 -19
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +2 -1
- package/src/crypto/__tests__/keyManager.test.ts +9 -7
- package/src/crypto/__tests__/signChallengeShared.test.ts +2 -1
- package/src/crypto/__tests__/signedRecord.test.ts +37 -150
- package/src/crypto/keyManager.ts +28 -17
- package/src/crypto/polyfill.ts +5 -4
- package/src/crypto/signatureService.ts +67 -255
- package/src/i18n/locales/en-US.json +391 -17
- package/src/i18n/locales/es-ES.json +391 -17
- package/src/index.ts +3 -3
- package/src/mixins/OxyServices.accounts.ts +91 -3
- package/src/mixins/OxyServices.assets.ts +1 -1
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.civic.ts +6 -17
- package/src/mixins/OxyServices.identity.ts +8 -2
- package/src/mixins/OxyServices.language.ts +1 -1
- package/src/mixins/OxyServices.nodes.ts +1 -12
- package/src/mixins/OxyServices.utility.ts +6 -119
- package/src/mixins/__tests__/OxyServices.civic.test.ts +2 -2
- package/src/mixins/__tests__/accounts.test.ts +70 -0
- package/src/server/auth.ts +0 -7
- package/src/server/index.ts +0 -1
- package/src/utils/__tests__/cacheKey.test.ts +0 -0
- package/src/utils/cacheKey.ts +16 -21
- package/src/utils/deviceManager.ts +1 -1
- package/src/utils/platform.ts +0 -14
- package/dist/cjs/crypto/canonicalJson.js +0 -107
- package/dist/cjs/utils/platformCrypto.js +0 -165
- package/dist/cjs/utils/platformCrypto.native.js +0 -123
- package/dist/esm/crypto/canonicalJson.js +0 -104
- package/dist/esm/utils/platformCrypto.js +0 -125
- package/dist/esm/utils/platformCrypto.native.js +0 -80
- package/dist/types/crypto/canonicalJson.d.ts +0 -44
- package/dist/types/utils/platformCrypto.d.ts +0 -87
- package/dist/types/utils/platformCrypto.native.d.ts +0 -54
- package/src/crypto/__tests__/canonicalJson.test.ts +0 -116
- package/src/crypto/canonicalJson.ts +0 -120
- package/src/utils/platformCrypto.native.ts +0 -101
- package/src/utils/platformCrypto.ts +0 -145
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant, RNFileDescriptor } from '../models/interfaces';
|
|
2
2
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
3
|
-
import { isReactNative } from '
|
|
3
|
+
import { isReactNative } from '@oxyhq/protocol';
|
|
4
4
|
|
|
5
5
|
export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
6
6
|
return class extends Base {
|
|
@@ -16,7 +16,7 @@ import type { PublicApplication } from './OxyServices.connectedApps';
|
|
|
16
16
|
import { OxyAuthenticationError } from '../OxyServices.errors';
|
|
17
17
|
import { KeyManager } from '../crypto/keyManager';
|
|
18
18
|
import { SignatureService } from '../crypto/signatureService';
|
|
19
|
-
import { loadNodeCrypto } from '
|
|
19
|
+
import { loadNodeCrypto } from '@oxyhq/protocol';
|
|
20
20
|
import { logger } from '../utils/loggerUtils';
|
|
21
21
|
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
|
|
22
22
|
|
|
@@ -49,17 +49,18 @@
|
|
|
49
49
|
* session.
|
|
50
50
|
*/
|
|
51
51
|
import type {
|
|
52
|
+
ChainHeadResponse,
|
|
52
53
|
CredentialIssueResult,
|
|
53
54
|
CredentialListResult,
|
|
54
55
|
CredentialStatus,
|
|
55
56
|
CredentialVerifyResult,
|
|
56
57
|
ExportAttestation,
|
|
58
|
+
OxySignedRecordType,
|
|
57
59
|
PersonhoodStatusResult,
|
|
58
60
|
PublicCard,
|
|
59
61
|
RealLifeAttestationResult,
|
|
60
62
|
SignedPublicCard,
|
|
61
63
|
SignedRecordEnvelope,
|
|
62
|
-
SignedRecordType,
|
|
63
64
|
ValidationRequestSummary,
|
|
64
65
|
ValidationVerdict,
|
|
65
66
|
ValidationVoteResult,
|
|
@@ -67,7 +68,7 @@ import type {
|
|
|
67
68
|
VouchResult,
|
|
68
69
|
} from '@oxyhq/contracts';
|
|
69
70
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
70
|
-
import { canonicalize } from '
|
|
71
|
+
import { canonicalize, verifySignature } from '@oxyhq/protocol';
|
|
71
72
|
import { SignatureService } from '../crypto/signatureService';
|
|
72
73
|
import { buildUserDid } from './OxyServices.identity';
|
|
73
74
|
import { CACHE_TIMES } from './mixinHelpers';
|
|
@@ -290,7 +291,7 @@ export function parseAttestPayload(raw: string): ParsedAttestPayload | null {
|
|
|
290
291
|
* key, matching the server which omits absent keys entirely) and checks the
|
|
291
292
|
* `ES256K-DER-SHA256` signature against `attestation.publicKey`.
|
|
292
293
|
*
|
|
293
|
-
* NEVER throws: `
|
|
294
|
+
* NEVER throws: `verifySignature` already swallows malformed-input
|
|
294
295
|
* errors and returns `false`, and an absent attestation short-circuits to
|
|
295
296
|
* `false`. A pure, reusable helper (Commons can call it on a cached card).
|
|
296
297
|
*
|
|
@@ -308,7 +309,7 @@ export async function verifyPublicCardAttestation(
|
|
|
308
309
|
if (!signature || !publicKey) {
|
|
309
310
|
return false;
|
|
310
311
|
}
|
|
311
|
-
return
|
|
312
|
+
return verifySignature(canonicalize(card), signature, publicKey);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
/**
|
|
@@ -390,18 +391,6 @@ export interface RevokeCredentialResult {
|
|
|
390
391
|
credential: VerifiableCredentialResponse;
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
/**
|
|
394
|
-
* The current chain head as returned by `GET /identity/records/:userId/chain/head`.
|
|
395
|
-
* `headRecordId` is `null` and `seq` is `-1` when the subject has no chain yet,
|
|
396
|
-
* so the next record's coordinates are always `seq: head.seq + 1` (genesis = 0)
|
|
397
|
-
* and `prev: head.headRecordId` (genesis = null).
|
|
398
|
-
*/
|
|
399
|
-
interface ChainHeadResponse {
|
|
400
|
-
headRecordId: string | null;
|
|
401
|
-
seq: number;
|
|
402
|
-
recordCount: number;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
394
|
export function OxyServicesCivicMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
406
395
|
return class extends Base {
|
|
407
396
|
constructor(...args: any[]) {
|
|
@@ -929,7 +918,7 @@ export function OxyServicesCivicMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
929
918
|
* @param rkey - The AtProto-style record key within the collection.
|
|
930
919
|
*/
|
|
931
920
|
async _signMyCivicRecordV2(
|
|
932
|
-
type:
|
|
921
|
+
type: OxySignedRecordType,
|
|
933
922
|
record: Record<string, unknown>,
|
|
934
923
|
collection: string,
|
|
935
924
|
rkey: string,
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
DidDocument,
|
|
29
29
|
DomainVerificationInstructions,
|
|
30
30
|
ExportBundle,
|
|
31
|
+
OxySignedRecordType,
|
|
31
32
|
SignedRecordEnvelope,
|
|
32
33
|
VerifiedDomain,
|
|
33
34
|
} from '@oxyhq/contracts';
|
|
@@ -43,8 +44,13 @@ import { CACHE_TIMES } from './mixinHelpers';
|
|
|
43
44
|
*/
|
|
44
45
|
const OXY_IDENTITY_APEX = 'oxy.so';
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Record categories a client may sign and publish to the Oxy store. The base
|
|
49
|
+
* envelope `type` is now an open string (any app may define its own records on
|
|
50
|
+
* the shared grammar); the Oxy identity store re-narrows it to the closed Oxy
|
|
51
|
+
* record set.
|
|
52
|
+
*/
|
|
53
|
+
export type IdentityRecordType = OxySignedRecordType;
|
|
48
54
|
|
|
49
55
|
/** Auth-method types that can be unlinked via {@link OxyServicesIdentityMixin}. */
|
|
50
56
|
export type UnlinkableAuthMethodType = 'identity' | 'password' | 'google' | 'apple' | 'github';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
|
|
5
5
|
import type { LanguageMetadata } from '../utils/languageUtils';
|
|
6
6
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
7
|
-
import { loadAsyncStorage } from '
|
|
7
|
+
import { loadAsyncStorage } from '@oxyhq/protocol';
|
|
8
8
|
import { isDev } from '../shared/utils/debugUtils';
|
|
9
9
|
|
|
10
10
|
export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* server's `serializeNode` projection exactly. Dates cross the wire as ISO
|
|
41
41
|
* strings.
|
|
42
42
|
*/
|
|
43
|
+
import type { ChainHeadResponse } from '@oxyhq/contracts';
|
|
43
44
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
44
45
|
import { SignatureService } from '../crypto/signatureService';
|
|
45
46
|
import { buildUserDid } from './OxyServices.identity';
|
|
@@ -157,18 +158,6 @@ export interface RemoveNodeResult {
|
|
|
157
158
|
revoked: boolean;
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
/**
|
|
161
|
-
* The current chain head as returned by `GET /identity/records/:userId/chain/head`.
|
|
162
|
-
* `headRecordId` is `null` and `seq` is `-1` when the subject has no chain yet,
|
|
163
|
-
* so the next record's coordinates are always `seq: head.seq + 1` (genesis = 0)
|
|
164
|
-
* and `prev: head.headRecordId` (genesis = null).
|
|
165
|
-
*/
|
|
166
|
-
interface ChainHeadResponse {
|
|
167
|
-
headRecordId: string | null;
|
|
168
|
-
seq: number;
|
|
169
|
-
recordCount: number;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
161
|
export function OxyServicesNodesMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
173
162
|
return class extends Base {
|
|
174
163
|
constructor(...args: any[]) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { jwtDecode } from 'jwt-decode';
|
|
8
8
|
import type { ApiError, User } from '../models/interfaces';
|
|
9
9
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
10
|
-
import { loadNodeCrypto } from '
|
|
10
|
+
import { loadNodeCrypto } from '@oxyhq/protocol';
|
|
11
11
|
import { logger } from '../utils/loggerUtils';
|
|
12
12
|
import { CACHE_TIMES } from './mixinHelpers';
|
|
13
13
|
|
|
@@ -26,17 +26,6 @@ interface JwtPayload {
|
|
|
26
26
|
[key: string]: unknown;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Result from the account acting-as verification endpoint
|
|
31
|
-
* (`GET /accounts/verify-acting-as`). Indicates whether a user is authorized to
|
|
32
|
-
* act as a given account. The `account:act_as` capability is granted only to the
|
|
33
|
-
* `owner`, `admin`, and `editor` account roles.
|
|
34
|
-
*/
|
|
35
|
-
interface ActingAsVerification {
|
|
36
|
-
authorized: boolean;
|
|
37
|
-
role: 'owner' | 'admin' | 'editor';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
/**
|
|
41
30
|
* Result from the service-acting-as verification endpoint.
|
|
42
31
|
* Confirms that a given service app holds an active delegation grant for
|
|
@@ -141,9 +130,6 @@ interface AuthMiddlewareOptions {
|
|
|
141
130
|
|
|
142
131
|
export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
143
132
|
return class extends Base {
|
|
144
|
-
/** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
|
|
145
|
-
_actingAsCache = new Map<string, { result: ActingAsVerification | null; expiresAt: number }>();
|
|
146
|
-
|
|
147
133
|
/**
|
|
148
134
|
* In-memory cache for service-acting-as verification.
|
|
149
135
|
* Negative results are cached for 1min to avoid hammering the verify
|
|
@@ -161,55 +147,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
161
147
|
super(...(args as [any]));
|
|
162
148
|
}
|
|
163
149
|
|
|
164
|
-
/**
|
|
165
|
-
* Verify that a user is authorized to act as an account (direct membership
|
|
166
|
-
* or inherited via an ancestor). Backed by `GET /accounts/verify-acting-as`.
|
|
167
|
-
* Results are cached in-memory for 5 minutes to avoid repeated API calls.
|
|
168
|
-
*
|
|
169
|
-
* @internal Used by the auth() middleware — not part of the public API
|
|
170
|
-
*/
|
|
171
|
-
async verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null> {
|
|
172
|
-
const cacheKey = `${userId}:${accountId}`;
|
|
173
|
-
const now = Date.now();
|
|
174
|
-
|
|
175
|
-
// Check cache
|
|
176
|
-
const cached = this._actingAsCache.get(cacheKey);
|
|
177
|
-
if (cached && cached.expiresAt > now) {
|
|
178
|
-
return cached.result;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Query the API
|
|
182
|
-
try {
|
|
183
|
-
const result = await this.makeRequest<ActingAsVerification>(
|
|
184
|
-
'GET',
|
|
185
|
-
'/accounts/verify-acting-as',
|
|
186
|
-
{ accountId, userId },
|
|
187
|
-
{ cache: false, retry: false, timeout: 5000 }
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Cache successful result for 5 minutes
|
|
191
|
-
this._actingAsCache.set(cacheKey, {
|
|
192
|
-
result: result && result.authorized ? result : null,
|
|
193
|
-
expiresAt: now + 5 * 60 * 1000,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return result && result.authorized ? result : null;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
|
|
199
|
-
component: 'auth',
|
|
200
|
-
method: 'verifyActingAs',
|
|
201
|
-
userId,
|
|
202
|
-
accountId,
|
|
203
|
-
}, error);
|
|
204
|
-
// Cache negative result for 1 minute to avoid hammering on transient errors
|
|
205
|
-
this._actingAsCache.set(cacheKey, {
|
|
206
|
-
result: null,
|
|
207
|
-
expiresAt: now + 1 * 60 * 1000,
|
|
208
|
-
});
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
150
|
/**
|
|
214
151
|
* Verify that a service app holds an active delegation grant authorising
|
|
215
152
|
* it to act on behalf of `userId`. Returns the grant (with allowed scopes)
|
|
@@ -362,50 +299,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
362
299
|
|
|
363
300
|
// Return an async middleware function
|
|
364
301
|
return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
|
|
365
|
-
// Process X-Acting-As header for managed account identity delegation.
|
|
366
|
-
// Called after successful authentication, before next(). If the header
|
|
367
|
-
// is present, verifies authorization and swaps the request identity to
|
|
368
|
-
// the managed account, preserving the original user for audit trails.
|
|
369
|
-
const processActingAs = async (): Promise<boolean> => {
|
|
370
|
-
const actingAsUserId = req.headers['x-acting-as'];
|
|
371
|
-
if (!actingAsUserId || typeof actingAsUserId !== 'string') return true; // No header, proceed normally
|
|
372
|
-
const currentUserId = req.userId;
|
|
373
|
-
if (!currentUserId) return true; // No authenticated user yet — nothing to swap
|
|
374
|
-
|
|
375
|
-
const verification = await oxyInstance.verifyActingAs(currentUserId, actingAsUserId);
|
|
376
|
-
if (!verification) {
|
|
377
|
-
const error = {
|
|
378
|
-
error: 'ACTING_AS_UNAUTHORIZED',
|
|
379
|
-
message: 'Not authorized to act as this account',
|
|
380
|
-
code: 'ACTING_AS_UNAUTHORIZED',
|
|
381
|
-
status: 403,
|
|
382
|
-
};
|
|
383
|
-
if (onError) {
|
|
384
|
-
onError(error);
|
|
385
|
-
} else {
|
|
386
|
-
res.status(403).json(error);
|
|
387
|
-
}
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Preserve original user for audit trails
|
|
392
|
-
req.originalUser = { id: currentUserId, ...(req.user ?? {}) };
|
|
393
|
-
req.actingAs = { userId: actingAsUserId, role: verification.role };
|
|
394
|
-
|
|
395
|
-
// Swap user identity to the managed account
|
|
396
|
-
req.userId = actingAsUserId;
|
|
397
|
-
req.user = { id: actingAsUserId, _id: actingAsUserId } as unknown as User;
|
|
398
|
-
|
|
399
|
-
if (debug) {
|
|
400
|
-
logger.debug(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${currentUserId}`, {
|
|
401
|
-
component: 'auth',
|
|
402
|
-
method: 'auth.processActingAs',
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return true;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
302
|
try {
|
|
410
303
|
// Extract token from Authorization header.
|
|
411
304
|
// Node/Express normalizes `Authorization` to a string; we guard
|
|
@@ -490,9 +383,9 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
490
383
|
// Signature verification uses a manual HMAC-SHA256 compare because
|
|
491
384
|
// this file ships into RN/web bundles where `jsonwebtoken` is
|
|
492
385
|
// unavailable. The middleware only ever runs on Node hosts (see
|
|
493
|
-
//
|
|
494
|
-
// platform: the RN variant throws so Metro
|
|
495
|
-
// built-in reference.
|
|
386
|
+
// `@oxyhq/protocol`'s `platform/crypto` doc-comment), and
|
|
387
|
+
// `loadNodeCrypto` is per-platform: the RN variant throws so Metro
|
|
388
|
+
// never bundles a Node built-in reference.
|
|
496
389
|
try {
|
|
497
390
|
await verifyServiceTokenSignature(token, jwtSecret);
|
|
498
391
|
verifyServiceTokenClaims(decoded, {
|
|
@@ -712,9 +605,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
712
605
|
});
|
|
713
606
|
}
|
|
714
607
|
|
|
715
|
-
|
|
716
|
-
if (await processActingAs()) return next();
|
|
717
|
-
return;
|
|
608
|
+
return next();
|
|
718
609
|
} catch (validationError) {
|
|
719
610
|
if (debug) {
|
|
720
611
|
logger.debug('[oxy.auth] Session validation failed', {
|
|
@@ -781,8 +672,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
781
672
|
});
|
|
782
673
|
}
|
|
783
674
|
|
|
784
|
-
|
|
785
|
-
if (await processActingAs()) next();
|
|
675
|
+
next();
|
|
786
676
|
} catch (error) {
|
|
787
677
|
const handled = oxyInstance.handleError(error) as Error & {
|
|
788
678
|
code?: string;
|
|
@@ -1128,8 +1018,6 @@ interface AuthReq {
|
|
|
1128
1018
|
sessionId?: string | null;
|
|
1129
1019
|
serviceApp?: ServiceApp;
|
|
1130
1020
|
serviceActingAs?: { userId: string; scopes: string[] };
|
|
1131
|
-
actingAs?: { userId: string; role: string };
|
|
1132
|
-
originalUser?: { id: string } & Partial<User>;
|
|
1133
1021
|
}
|
|
1134
1022
|
|
|
1135
1023
|
interface AuthRes {
|
|
@@ -1146,7 +1034,6 @@ interface SocketLike {
|
|
|
1146
1034
|
}
|
|
1147
1035
|
|
|
1148
1036
|
interface OxyAuthInstance {
|
|
1149
|
-
verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null>;
|
|
1150
1037
|
verifyServiceActingAs(appId: string, userId: string): Promise<ServiceActingAsVerification | null>;
|
|
1151
1038
|
validateSession(
|
|
1152
1039
|
sessionId: string,
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
VerifiableCredentialResponse,
|
|
38
38
|
} from '@oxyhq/contracts';
|
|
39
39
|
import { OxyServices } from '../../OxyServices';
|
|
40
|
-
import { canonicalize } from '
|
|
40
|
+
import { canonicalize, signMessage } from '@oxyhq/protocol';
|
|
41
41
|
import { SignatureService } from '../../crypto/signatureService';
|
|
42
42
|
import { parseAttestPayload, parseIdPayload, verifyPublicCardAttestation } from '../OxyServices.civic';
|
|
43
43
|
|
|
@@ -61,7 +61,7 @@ async function signCard(card: PublicCard): Promise<{ attestation: ExportAttestat
|
|
|
61
61
|
const keyPair = ec.genKeyPair();
|
|
62
62
|
const privateKey = keyPair.getPrivate('hex');
|
|
63
63
|
const publicKey = keyPair.getPublic('hex');
|
|
64
|
-
const signature = await
|
|
64
|
+
const signature = await signMessage(canonicalize(card), privateKey);
|
|
65
65
|
return {
|
|
66
66
|
attestation: {
|
|
67
67
|
issuer: 'did:web:api.oxy.so',
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ApplicationCredentialWithSecret,
|
|
26
26
|
RotateApplicationCredentialResult,
|
|
27
27
|
ApplicationUsageStats,
|
|
28
|
+
SwitchAccountResult,
|
|
28
29
|
} from '../OxyServices.accounts';
|
|
29
30
|
|
|
30
31
|
const setAccessTokenForTest = (oxy: OxyServices): void => {
|
|
@@ -174,6 +175,75 @@ describe('OxyServices.accounts', () => {
|
|
|
174
175
|
});
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
describe('switchToAccount', () => {
|
|
179
|
+
const switchResponse: SwitchAccountResult = {
|
|
180
|
+
sessionId: 'sess_switch',
|
|
181
|
+
deviceId: 'dev_switch',
|
|
182
|
+
expiresAt: '2026-06-30T01:00:00.000Z',
|
|
183
|
+
accessToken: 'access_switch',
|
|
184
|
+
user: { id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } },
|
|
185
|
+
authuser: 2,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
it('posts to /:id/switch (no body, no cache), plants the token, sweeps the cache, and returns the session', async () => {
|
|
189
|
+
const setTokensSpy = jest.spyOn(oxy, 'setTokens');
|
|
190
|
+
const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
|
|
191
|
+
makeRequestSpy.mockResolvedValue(switchResponse);
|
|
192
|
+
|
|
193
|
+
const result = await oxy.switchToAccount('acc1');
|
|
194
|
+
|
|
195
|
+
// Request shape: POST, exact path, no body, cache disabled.
|
|
196
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
197
|
+
'POST',
|
|
198
|
+
'/accounts/acc1/switch',
|
|
199
|
+
undefined,
|
|
200
|
+
expect.objectContaining({ cache: false }),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Session planting: the access token from the body is installed as the
|
|
204
|
+
// active token (mirrors claimSessionByToken / verifyChallenge).
|
|
205
|
+
expect(setTokensSpy).toHaveBeenCalledWith('access_switch');
|
|
206
|
+
expect(oxy.getAccessToken()).toBe('access_switch');
|
|
207
|
+
expect(oxy.hasValidToken()).toBe(true);
|
|
208
|
+
|
|
209
|
+
// Identity changed → the whole GET cache is swept so reads refetch as the
|
|
210
|
+
// new account, AND it happens AFTER the token is planted.
|
|
211
|
+
expect(clearCacheSpy).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(setTokensSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
|
213
|
+
clearCacheSpy.mock.invocationCallOrder[0],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// The returned session carries the target account (id-normalised) + authuser.
|
|
217
|
+
expect(result).toEqual({ ...switchResponse, user: { id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } } });
|
|
218
|
+
expect(result.authuser).toBe(2);
|
|
219
|
+
|
|
220
|
+
setTokensSpy.mockRestore();
|
|
221
|
+
clearCacheSpy.mockRestore();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('URL-encodes the accountId path segment', async () => {
|
|
225
|
+
makeRequestSpy.mockResolvedValue(switchResponse);
|
|
226
|
+
await oxy.switchToAccount('a b/c');
|
|
227
|
+
expect(makeRequestSpy.mock.calls[0][1]).toBe('/accounts/a%20b%2Fc/switch');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does NOT plant or sweep when the operator is not authorized (403 surfaces via handleError)', async () => {
|
|
231
|
+
const setTokensSpy = jest.spyOn(oxy, 'setTokens');
|
|
232
|
+
const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
|
|
233
|
+
makeRequestSpy.mockRejectedValue(
|
|
234
|
+
Object.assign(new Error('forbidden'), { response: { status: 403 } }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await expect(oxy.switchToAccount('acc1')).rejects.toThrow();
|
|
238
|
+
// A failed switch must NOT mutate session state.
|
|
239
|
+
expect(setTokensSpy).not.toHaveBeenCalled();
|
|
240
|
+
expect(clearCacheSpy).not.toHaveBeenCalled();
|
|
241
|
+
|
|
242
|
+
setTokensSpy.mockRestore();
|
|
243
|
+
clearCacheSpy.mockRestore();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
177
247
|
describe('createAccount', () => {
|
|
178
248
|
it('posts the payload, unwraps `account`, and busts every list', async () => {
|
|
179
249
|
makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
|
package/src/server/auth.ts
CHANGED
|
@@ -10,11 +10,6 @@ export interface OxyRequestUser {
|
|
|
10
10
|
[key: string]: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface OxyActingAsContext {
|
|
14
|
-
userId: string;
|
|
15
|
-
role: 'owner' | 'admin' | 'editor';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
export interface OxyServiceAppContext {
|
|
19
14
|
appId: string;
|
|
20
15
|
appName: string;
|
|
@@ -30,8 +25,6 @@ export interface OxyServiceActingAsContext {
|
|
|
30
25
|
export interface OxyAuthRequest extends Request {
|
|
31
26
|
userId?: string | null;
|
|
32
27
|
user?: OxyRequestUser | null;
|
|
33
|
-
originalUser?: OxyRequestUser | null;
|
|
34
|
-
actingAs?: OxyActingAsContext;
|
|
35
28
|
accessToken?: string;
|
|
36
29
|
sessionId?: string | null;
|
|
37
30
|
serviceApp?: OxyServiceAppContext;
|
package/src/server/index.ts
CHANGED
|
Binary file
|
package/src/utils/cacheKey.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from {@link HttpService} so the identity-tag derivation is a pure,
|
|
5
5
|
* independently testable function with no dependency on instance/token state.
|
|
6
|
-
* The HTTP service injects the live access token
|
|
7
|
-
*
|
|
6
|
+
* The HTTP service injects the live access token; everything here is
|
|
7
|
+
* referentially transparent given that input.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { jwtDecode } from 'jwt-decode';
|
|
@@ -72,27 +72,22 @@ export function fnv1a32(str: string): string {
|
|
|
72
72
|
*
|
|
73
73
|
* We use the decoded user id rather than the raw JWT so the token never lands
|
|
74
74
|
* in a cache key (no token leakage through any cache-key logging, no key bloat).
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* Switching into a managed account mints a REAL new session whose access token
|
|
76
|
+
* carries the target account's id, so the identity tag changes naturally on a
|
|
77
|
+
* switch — there is no separate acting-as discriminator to fold in.
|
|
78
78
|
*
|
|
79
79
|
* @param accessToken The current bearer access token, or `null` when anonymous.
|
|
80
|
-
* @param actingAsUserId The active managed-account id, or `null`.
|
|
81
80
|
*/
|
|
82
|
-
export function computeIdentityTag(
|
|
83
|
-
accessToken
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// tokens via a hash. Never silently fall back to ANON_IDENTITY.
|
|
94
|
-
principal = `t${fnv1a32(accessToken)}`;
|
|
95
|
-
}
|
|
81
|
+
export function computeIdentityTag(accessToken: string | null): string {
|
|
82
|
+
if (!accessToken) {
|
|
83
|
+
return ANON_IDENTITY;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const decoded = jwtDecode<CacheIdentityJwtPayload>(accessToken);
|
|
87
|
+
return decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
|
|
88
|
+
} catch {
|
|
89
|
+
// Undecodable token — still partition it away from anon and from other
|
|
90
|
+
// tokens via a hash. Never silently fall back to ANON_IDENTITY.
|
|
91
|
+
return `t${fnv1a32(accessToken)}`;
|
|
96
92
|
}
|
|
97
|
-
return actingAsUserId ? `${principal}~as${actingAsUserId}` : principal;
|
|
98
93
|
}
|
package/src/utils/platform.ts
CHANGED
|
@@ -88,20 +88,6 @@ export function isAndroid(): boolean {
|
|
|
88
88
|
return getPlatformOS() === 'android';
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
* Check if running in React Native
|
|
93
|
-
*/
|
|
94
|
-
export function isReactNative(): boolean {
|
|
95
|
-
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Check if running in Node.js
|
|
100
|
-
*/
|
|
101
|
-
export function isNodeJS(): boolean {
|
|
102
|
-
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
91
|
/**
|
|
106
92
|
* Set the platform OS explicitly
|
|
107
93
|
* Called by React Native entry point to register the platform
|