@oxyhq/core 4.0.1 → 5.1.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/index.js +4 -8
- package/dist/cjs/mixins/OxyServices.accounts.js +90 -0
- package/dist/cjs/mixins/OxyServices.assets.js +68 -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/index.js +1 -2
- package/dist/esm/mixins/OxyServices.accounts.js +90 -0
- package/dist/esm/mixins/OxyServices.assets.js +67 -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 +4 -5
- package/dist/types/mixins/OxyServices.accounts.d.ts +66 -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 +42 -3
- 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/models/interfaces.d.ts +18 -0
- 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/index.ts +4 -3
- package/src/mixins/OxyServices.accounts.ts +136 -3
- package/src/mixins/OxyServices.assets.ts +96 -2
- 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__/OxyServices.serviceAssetMetadata.test.ts +116 -0
- package/src/mixins/__tests__/accounts.test.ts +120 -0
- package/src/models/interfaces.ts +19 -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
package/dist/esm/HttpService.js
CHANGED
|
@@ -17,7 +17,8 @@ import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/request
|
|
|
17
17
|
import { retryAsync } from './utils/asyncUtils.js';
|
|
18
18
|
import { handleHttpError } from './utils/errorUtils.js';
|
|
19
19
|
import { jwtDecode } from 'jwt-decode';
|
|
20
|
-
import { isNative,
|
|
20
|
+
import { isNative, getPlatformOS } from './utils/platform.js';
|
|
21
|
+
import { isReactNative } from '@oxyhq/protocol';
|
|
21
22
|
import { computeIdentityTag, fnv1a32 } from './utils/cacheKey.js';
|
|
22
23
|
/**
|
|
23
24
|
* Check if we're running in a native app environment (React Native, not web)
|
|
@@ -146,8 +147,6 @@ export class HttpService {
|
|
|
146
147
|
* Each listener receives the resulting access token, or `null` when cleared.
|
|
147
148
|
*/
|
|
148
149
|
this._tokenChangeListeners = new Set();
|
|
149
|
-
// Acting-as identity for managed accounts
|
|
150
|
-
this._actingAsUserId = null;
|
|
151
150
|
// Performance monitoring
|
|
152
151
|
this.requestMetrics = {
|
|
153
152
|
totalRequests: 0,
|
|
@@ -321,10 +320,6 @@ export class HttpService {
|
|
|
321
320
|
hasNativeAppHeader: headers['X-Native-App'] === 'true',
|
|
322
321
|
});
|
|
323
322
|
}
|
|
324
|
-
// Add X-Acting-As header for managed account identity delegation
|
|
325
|
-
if (this._actingAsUserId) {
|
|
326
|
-
headers['X-Acting-As'] = this._actingAsUserId;
|
|
327
|
-
}
|
|
328
323
|
// Merge custom headers if provided
|
|
329
324
|
if (config.headers) {
|
|
330
325
|
Object.entries(config.headers).forEach(([key, value]) => {
|
|
@@ -630,12 +625,12 @@ export class HttpService {
|
|
|
630
625
|
* Derive a stable, non-sensitive identity discriminator for cache scoping.
|
|
631
626
|
*
|
|
632
627
|
* Thin instance wrapper over the pure {@link computeIdentityTag} helper —
|
|
633
|
-
* binds it to this instance's live access token
|
|
634
|
-
*
|
|
635
|
-
*
|
|
628
|
+
* binds it to this instance's live access token. See that function's docs for
|
|
629
|
+
* the full resolution contract (anon fallback, decoded `userId || id`,
|
|
630
|
+
* token-hash fallback for undecodable tokens).
|
|
636
631
|
*/
|
|
637
632
|
computeIdentityTag() {
|
|
638
|
-
return computeIdentityTag(this.tokenStore.getAccessToken()
|
|
633
|
+
return computeIdentityTag(this.tokenStore.getAccessToken());
|
|
639
634
|
}
|
|
640
635
|
/**
|
|
641
636
|
* Generate cache key efficiently
|
|
@@ -882,13 +877,6 @@ export class HttpService {
|
|
|
882
877
|
async delete(url, config) {
|
|
883
878
|
return this.request({ method: 'DELETE', url, ...config });
|
|
884
879
|
}
|
|
885
|
-
// Acting-as identity management (managed accounts)
|
|
886
|
-
setActingAs(userId) {
|
|
887
|
-
this._actingAsUserId = userId;
|
|
888
|
-
}
|
|
889
|
-
getActingAs() {
|
|
890
|
-
return this._actingAsUserId;
|
|
891
|
-
}
|
|
892
880
|
// Token management
|
|
893
881
|
setTokens(accessToken) {
|
|
894
882
|
this.tokenStore.setTokens(accessToken);
|
|
@@ -275,27 +275,6 @@ export class OxyServicesBase {
|
|
|
275
275
|
return null;
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Set the acting-as identity for managed accounts.
|
|
280
|
-
*
|
|
281
|
-
* When set, all subsequent API requests will include the `X-Acting-As` header,
|
|
282
|
-
* causing the server to attribute actions to the managed account. The
|
|
283
|
-
* authenticated user must be an authorized manager of the target account.
|
|
284
|
-
*
|
|
285
|
-
* Pass `null` to clear and revert to the authenticated user's own identity.
|
|
286
|
-
*
|
|
287
|
-
* @param userId - The managed account user ID, or null to clear
|
|
288
|
-
*/
|
|
289
|
-
setActingAs(userId) {
|
|
290
|
-
this.httpService.setActingAs(userId);
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Get the current acting-as identity (managed account user ID), or null
|
|
294
|
-
* if operating as the authenticated user's own identity.
|
|
295
|
-
*/
|
|
296
|
-
getActingAs() {
|
|
297
|
-
return this.httpService.getActingAs();
|
|
298
|
-
}
|
|
299
278
|
/**
|
|
300
279
|
* Wait for authentication to be ready
|
|
301
280
|
*
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import _cjs_elliptic from 'elliptic';
|
|
8
8
|
const { ec: EC } = _cjs_elliptic;
|
|
9
|
-
import { isWeb, isIOS, isAndroid
|
|
10
|
-
import { loadExpoCrypto, loadNodeCrypto, loadSecureStore } from '
|
|
9
|
+
import { isWeb, isIOS, isAndroid } from '../utils/platform.js';
|
|
10
|
+
import { isReactNative, isNodeJS, loadExpoCrypto, loadNodeCrypto, loadSecureStore } from '@oxyhq/protocol';
|
|
11
11
|
import { logger } from '../utils/loggerUtils.js';
|
|
12
12
|
import { isDev } from '../shared/utils/debugUtils.js';
|
|
13
13
|
/**
|
|
@@ -68,8 +68,8 @@ const ANDROID_ACCOUNT_TYPE = 'com.oxy.account';
|
|
|
68
68
|
/**
|
|
69
69
|
* Initialize React Native specific modules
|
|
70
70
|
*
|
|
71
|
-
* Delegates to `
|
|
72
|
-
* (`
|
|
71
|
+
* Delegates to `@oxyhq/protocol`'s `platform/crypto`, a per-platform module
|
|
72
|
+
* (`crypto.ts` vs `crypto.native.ts`) selected by the
|
|
73
73
|
* consumer's bundler. On RN it returns a statically-imported handle to
|
|
74
74
|
* `expo-secure-store`; off RN it throws (and is never called because every
|
|
75
75
|
* caller is gated by `isWebPlatform()` / native-only paths).
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* - Browser/Node.js: Uses native crypto
|
|
8
8
|
* - React Native: Uses expo-crypto (statically imported via the
|
|
9
|
-
* per-platform `
|
|
10
|
-
* how platform routing works).
|
|
9
|
+
* per-platform `platform/crypto` module in `@oxyhq/protocol` — see that
|
|
10
|
+
* file's doc-comment for how platform routing works).
|
|
11
11
|
*/
|
|
12
12
|
import _cjs_buffer from 'buffer';
|
|
13
13
|
const { Buffer } = _cjs_buffer;
|
|
14
|
-
import { getRandomBytesRN } from '
|
|
14
|
+
import { getRandomBytesRN } from '@oxyhq/protocol';
|
|
15
15
|
const getGlobalObject = () => {
|
|
16
16
|
if (typeof globalThis !== 'undefined')
|
|
17
17
|
return globalThis;
|
|
@@ -31,7 +31,8 @@ if (!globalObject.Buffer) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Synchronous random-bytes shim. On RN, this delegates to
|
|
33
33
|
* `expo-crypto.getRandomBytes` (statically imported by the RN variant of
|
|
34
|
-
* `
|
|
34
|
+
* `@oxyhq/protocol`'s `platform/crypto`, so available without any async
|
|
35
|
+
* warm-up). On Node /
|
|
35
36
|
* browser, this throws — but is never called there because both platforms
|
|
36
37
|
* already provide `globalThis.crypto.getRandomValues` natively.
|
|
37
38
|
*/
|
|
@@ -1,83 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signature Service - ECDSA Digital Signatures
|
|
2
|
+
* Signature Service - ECDSA Digital Signatures (device-key bound)
|
|
3
3
|
*
|
|
4
|
-
* Handles signing and verification of messages
|
|
5
|
-
*
|
|
4
|
+
* Handles signing and verification of messages with the user's DEVICE identity
|
|
5
|
+
* key (read from {@link KeyManager} / secure storage). All cryptography itself —
|
|
6
|
+
* canonical signing input, SHA-256, secp256k1 sign/verify, envelope assembly —
|
|
7
|
+
* is delegated to `@oxyhq/protocol`; this service only resolves the key from
|
|
8
|
+
* storage and orchestrates the protocol primitives.
|
|
6
9
|
*/
|
|
7
|
-
import
|
|
8
|
-
const { ec: EC } = _cjs_elliptic;
|
|
10
|
+
import { signEnvelope, signMessage, verifySignature, sha256, loadExpoCrypto, loadNodeCrypto, isReactNative, isNodeJS, } from '@oxyhq/protocol';
|
|
9
11
|
import { KeyManager } from './keyManager.js';
|
|
10
|
-
import { canonicalize } from './canonicalJson.js';
|
|
11
|
-
import { isReactNative, isNodeJS } from '../utils/platform.js';
|
|
12
|
-
import { loadExpoCrypto, loadNodeCrypto } from '../utils/platformCrypto.js';
|
|
13
12
|
import { logger } from '../utils/loggerUtils.js';
|
|
14
|
-
import { isDev } from '../shared/utils/debugUtils.js';
|
|
15
|
-
const ec = new EC('secp256k1');
|
|
16
|
-
/**
|
|
17
|
-
* Compute the canonical signing input for a signed-record envelope.
|
|
18
|
-
*
|
|
19
|
-
* This is the single definition of "what the signature covers". `@oxyhq/core`
|
|
20
|
-
* (client signing) and `@oxyhq/api` (server verification) both call this, so a
|
|
21
|
-
* record signed by a client and verified by the server cannot drift.
|
|
22
|
-
*
|
|
23
|
-
* - **v1**: the canonical JSON of `{version, type, subject, issuer, record,
|
|
24
|
-
* issuedAt}` — BYTE-IDENTICAL to the original scheme, so every signature
|
|
25
|
-
* already in production keeps verifying.
|
|
26
|
-
* - **v2**: the canonical JSON additionally includes the hash-chain fields
|
|
27
|
-
* `{seq, prev, collection, rkey}`. Because {@link canonicalize} sorts keys,
|
|
28
|
-
* the on-the-wire field order is irrelevant; the resulting canonical key
|
|
29
|
-
* order is `collection, issuedAt, issuer, prev, record, rkey, seq, subject,
|
|
30
|
-
* type, version`. `prev` is `null` at genesis (serialized as `null`, not
|
|
31
|
-
* omitted), so it is always part of the signed bytes.
|
|
32
|
-
*/
|
|
33
|
-
export function signedRecordSigningInput(fields) {
|
|
34
|
-
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
35
|
-
if (version === 2) {
|
|
36
|
-
const { seq, prev, collection, rkey } = fields;
|
|
37
|
-
return canonicalize({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
|
|
38
|
-
}
|
|
39
|
-
return canonicalize({ version, type, subject, issuer, record, issuedAt });
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Compute the `recordId` (content address) of a signed record: the SHA-256 hex
|
|
43
|
-
* digest of its canonical {@link signedRecordSigningInput}.
|
|
44
|
-
*
|
|
45
|
-
* Deterministic and stable across runtimes (it reuses the same canonicalization
|
|
46
|
-
* + SHA-256 the signature itself is built on). The recordId is what `prev`
|
|
47
|
-
* references in the per-subject hash chain, so `@oxyhq/core` (client) and
|
|
48
|
-
* `@oxyhq/api` (server) MUST compute it identically — both call this function.
|
|
49
|
-
* It is taken over the SIGNING input (excluding `publicKey`/`signature`), so it
|
|
50
|
-
* is a pure content address of the record's meaning, independent of who signed.
|
|
51
|
-
*/
|
|
52
|
-
export async function computeRecordId(fields) {
|
|
53
|
-
return sha256(signedRecordSigningInput(fields));
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Compute SHA-256 hash of a string
|
|
57
|
-
*/
|
|
58
|
-
async function sha256(message) {
|
|
59
|
-
// In React Native, use expo-crypto
|
|
60
|
-
if (isReactNative()) {
|
|
61
|
-
const Crypto = await loadExpoCrypto();
|
|
62
|
-
return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
|
|
63
|
-
}
|
|
64
|
-
if (isNodeJS()) {
|
|
65
|
-
try {
|
|
66
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
67
|
-
return nodeCrypto.createHash('sha256').update(message).digest('hex');
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
// Node crypto failed to load — log and fall through to Web Crypto API
|
|
71
|
-
logger.warn('[oxy.crypto] Node crypto unavailable, falling back to Web Crypto', { component: 'SignatureService' }, error);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Browser: use Web Crypto API
|
|
75
|
-
const encoder = new TextEncoder();
|
|
76
|
-
const data = encoder.encode(message);
|
|
77
|
-
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
|
|
78
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
79
|
-
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
80
|
-
}
|
|
81
13
|
export class SignatureService {
|
|
82
14
|
/**
|
|
83
15
|
* Generate a random challenge string (for offline use)
|
|
@@ -116,76 +48,15 @@ export class SignatureService {
|
|
|
116
48
|
return sha256(message);
|
|
117
49
|
}
|
|
118
50
|
/**
|
|
119
|
-
* Sign a message using the stored private key
|
|
51
|
+
* Sign a message using the stored device private key
|
|
120
52
|
* Returns the signature in DER format (hex encoded)
|
|
121
53
|
*/
|
|
122
54
|
static async sign(message) {
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
55
|
+
const privateKey = await KeyManager.getPrivateKey();
|
|
56
|
+
if (!privateKey) {
|
|
125
57
|
throw new Error('No identity found. Please create or import an identity first.');
|
|
126
58
|
}
|
|
127
|
-
|
|
128
|
-
const signature = keyPair.sign(messageHash);
|
|
129
|
-
return signature.toDER('hex');
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Sign a message with an explicit private key (without storing)
|
|
133
|
-
* Useful for one-time operations or testing
|
|
134
|
-
*/
|
|
135
|
-
static async signWithKey(message, privateKey) {
|
|
136
|
-
const keyPair = ec.keyFromPrivate(privateKey);
|
|
137
|
-
const messageHash = await sha256(message);
|
|
138
|
-
const signature = keyPair.sign(messageHash);
|
|
139
|
-
return signature.toDER('hex');
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Verify a signature against a message and public key
|
|
143
|
-
*
|
|
144
|
-
* Returns false on any error (invalid signature, malformed input, etc.).
|
|
145
|
-
* Errors are logged at debug level so they're available when troubleshooting
|
|
146
|
-
* signature mismatches but don't surface to the caller.
|
|
147
|
-
*/
|
|
148
|
-
static async verify(message, signature, publicKey) {
|
|
149
|
-
try {
|
|
150
|
-
const key = ec.keyFromPublic(publicKey, 'hex');
|
|
151
|
-
const messageHash = await sha256(message);
|
|
152
|
-
return key.verify(messageHash, signature);
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (isDev()) {
|
|
156
|
-
logger.debug('[oxy.crypto] verify() returned false', { component: 'SignatureService' }, error);
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Synchronous verification (for Node.js backend)
|
|
163
|
-
* Uses crypto module directly for hashing
|
|
164
|
-
* Note: This method should only be used in Node.js environments
|
|
165
|
-
*/
|
|
166
|
-
static verifySync(message, signature, publicKey) {
|
|
167
|
-
try {
|
|
168
|
-
if (!isNodeJS()) {
|
|
169
|
-
// In React Native, use async verify instead
|
|
170
|
-
throw new Error('verifySync should only be used in Node.js. Use verify() in React Native.');
|
|
171
|
-
}
|
|
172
|
-
// Intentionally using Function constructor here: this method is synchronous by design
|
|
173
|
-
// (Node.js backend hot-path) so we cannot use `await import()`. The Function constructor
|
|
174
|
-
// prevents Metro/bundlers from statically resolving the require. This is acceptable because
|
|
175
|
-
// verifySync is gated by isNodeJS() and will never execute in browser/RN environments.
|
|
176
|
-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
177
|
-
const getCrypto = new Function('return require("crypto")');
|
|
178
|
-
const crypto = getCrypto();
|
|
179
|
-
const key = ec.keyFromPublic(publicKey, 'hex');
|
|
180
|
-
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
|
|
181
|
-
return key.verify(messageHash, signature);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
if (isDev()) {
|
|
185
|
-
logger.debug('[oxy.crypto] verifySync() returned false', { component: 'SignatureService' }, error);
|
|
186
|
-
}
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
59
|
+
return signMessage(message, privateKey);
|
|
189
60
|
}
|
|
190
61
|
/**
|
|
191
62
|
* Create a signed message object with metadata
|
|
@@ -219,7 +90,7 @@ export class SignatureService {
|
|
|
219
90
|
}
|
|
220
91
|
// Verify signature
|
|
221
92
|
const messageWithTimestamp = `${message}:${timestamp}`;
|
|
222
|
-
return
|
|
93
|
+
return verifySignature(messageWithTimestamp, signature, publicKey);
|
|
223
94
|
}
|
|
224
95
|
/**
|
|
225
96
|
* Create a signed authentication challenge response
|
|
@@ -247,9 +118,10 @@ export class SignatureService {
|
|
|
247
118
|
* Mirrors {@link signChallenge} exactly — same message format
|
|
248
119
|
* (`auth:${publicKey}:${challenge}:${timestamp}`) so the server verification
|
|
249
120
|
* path is unchanged — but sources the shared public/private key from
|
|
250
|
-
* `KeyManager` and signs with
|
|
251
|
-
* same-device shared-keychain SSO (Mechanism A): a
|
|
252
|
-
* control of the shared identity to mint its own
|
|
121
|
+
* `KeyManager` and signs with the protocol's explicit-key {@link signMessage}.
|
|
122
|
+
* Used by "Sign in with Oxy" same-device shared-keychain SSO (Mechanism A): a
|
|
123
|
+
* sibling native app proves control of the shared identity to mint its own
|
|
124
|
+
* session.
|
|
253
125
|
*
|
|
254
126
|
* Throws if no shared identity exists (native-only; the shared keychain is
|
|
255
127
|
* unavailable on web).
|
|
@@ -262,7 +134,7 @@ export class SignatureService {
|
|
|
262
134
|
}
|
|
263
135
|
const timestamp = Date.now();
|
|
264
136
|
const message = `auth:${publicKey}:${challenge}:${timestamp}`;
|
|
265
|
-
const signature = await
|
|
137
|
+
const signature = await signMessage(message, privateKey);
|
|
266
138
|
return {
|
|
267
139
|
challenge: signature,
|
|
268
140
|
publicKey,
|
|
@@ -280,7 +152,7 @@ export class SignatureService {
|
|
|
280
152
|
return false;
|
|
281
153
|
}
|
|
282
154
|
const message = `auth:${publicKey}:${originalChallenge}:${timestamp}`;
|
|
283
|
-
return
|
|
155
|
+
return verifySignature(message, signature, publicKey);
|
|
284
156
|
}
|
|
285
157
|
/**
|
|
286
158
|
* Create a registration signature
|
|
@@ -328,9 +200,11 @@ export class SignatureService {
|
|
|
328
200
|
*
|
|
329
201
|
* The envelope is self-issued: `issuer` equals `subject` (the signer's DID).
|
|
330
202
|
* The signature covers the canonical JSON of every field EXCEPT `publicKey`
|
|
331
|
-
* and `signature` (
|
|
332
|
-
*
|
|
333
|
-
*
|
|
203
|
+
* and `signature`; `alg` is `ES256K-DER-SHA256` (secp256k1 over the SHA-256 of
|
|
204
|
+
* the canonical bytes, DER-encoded). The cryptography is delegated to the
|
|
205
|
+
* protocol's {@link signEnvelope}, which derives the (uncompressed-hex)
|
|
206
|
+
* `publicKey` from the stored device key — identical to the registered
|
|
207
|
+
* verification method.
|
|
334
208
|
*
|
|
335
209
|
* Requires a stored identity (native secure storage); throws if none exists.
|
|
336
210
|
*
|
|
@@ -339,41 +213,26 @@ export class SignatureService {
|
|
|
339
213
|
* @param record - The arbitrary record payload to attest to.
|
|
340
214
|
*/
|
|
341
215
|
static async signRecord(type, subject, record) {
|
|
342
|
-
const
|
|
343
|
-
if (!
|
|
216
|
+
const privateKey = await KeyManager.getPrivateKey();
|
|
217
|
+
if (!privateKey) {
|
|
344
218
|
throw new Error('No identity found. Please create or import an identity first.');
|
|
345
219
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const issuedAt = Date.now();
|
|
349
|
-
const signingInput = signedRecordSigningInput({
|
|
350
|
-
version,
|
|
351
|
-
type,
|
|
352
|
-
subject,
|
|
353
|
-
issuer,
|
|
354
|
-
record,
|
|
355
|
-
issuedAt,
|
|
356
|
-
});
|
|
357
|
-
const signature = await SignatureService.sign(signingInput);
|
|
358
|
-
return {
|
|
359
|
-
version,
|
|
220
|
+
return signEnvelope({
|
|
221
|
+
version: 1,
|
|
360
222
|
type,
|
|
361
223
|
subject,
|
|
362
|
-
issuer,
|
|
224
|
+
issuer: subject,
|
|
363
225
|
record,
|
|
364
|
-
issuedAt,
|
|
365
|
-
|
|
366
|
-
alg: 'ES256K-DER-SHA256',
|
|
367
|
-
signature,
|
|
368
|
-
};
|
|
226
|
+
issuedAt: Date.now(),
|
|
227
|
+
}, privateKey);
|
|
369
228
|
}
|
|
370
229
|
/**
|
|
371
230
|
* Build a signed-record envelope (v2) carrying the per-subject hash-chain
|
|
372
231
|
* fields.
|
|
373
232
|
*
|
|
374
233
|
* Identical to {@link signRecord} (self-issued: `issuer === subject`; same
|
|
375
|
-
* `ES256K-DER-SHA256` scheme
|
|
376
|
-
*
|
|
234
|
+
* `ES256K-DER-SHA256` scheme) but `version` is `2` and the signed bytes
|
|
235
|
+
* additionally cover the chain fields:
|
|
377
236
|
*
|
|
378
237
|
* @param type - The record category.
|
|
379
238
|
* @param subject - The subject DID the record is about (also the issuer).
|
|
@@ -388,57 +247,23 @@ export class SignatureService {
|
|
|
388
247
|
* none exists.
|
|
389
248
|
*/
|
|
390
249
|
static async signRecordV2(type, subject, record, chain) {
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
250
|
+
const privateKey = await KeyManager.getPrivateKey();
|
|
251
|
+
if (!privateKey) {
|
|
393
252
|
throw new Error('No identity found. Please create or import an identity first.');
|
|
394
253
|
}
|
|
395
|
-
const version = 2;
|
|
396
|
-
const issuer = subject;
|
|
397
|
-
const issuedAt = Date.now();
|
|
398
254
|
const { seq, prev, collection, rkey } = chain;
|
|
399
|
-
|
|
400
|
-
version,
|
|
255
|
+
return signEnvelope({
|
|
256
|
+
version: 2,
|
|
401
257
|
type,
|
|
402
258
|
subject,
|
|
403
|
-
issuer,
|
|
259
|
+
issuer: subject,
|
|
404
260
|
record,
|
|
405
|
-
issuedAt,
|
|
261
|
+
issuedAt: Date.now(),
|
|
406
262
|
seq,
|
|
407
263
|
prev,
|
|
408
264
|
collection,
|
|
409
265
|
rkey,
|
|
410
|
-
});
|
|
411
|
-
const signature = await SignatureService.sign(signingInput);
|
|
412
|
-
return {
|
|
413
|
-
version,
|
|
414
|
-
type,
|
|
415
|
-
subject,
|
|
416
|
-
issuer,
|
|
417
|
-
record,
|
|
418
|
-
issuedAt,
|
|
419
|
-
seq,
|
|
420
|
-
prev,
|
|
421
|
-
collection,
|
|
422
|
-
rkey,
|
|
423
|
-
publicKey,
|
|
424
|
-
alg: 'ES256K-DER-SHA256',
|
|
425
|
-
signature,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Verify a signed-record envelope: recompute the canonical signing input from
|
|
430
|
-
* the envelope's own fields and check the signature against the envelope's
|
|
431
|
-
* `publicKey`.
|
|
432
|
-
*
|
|
433
|
-
* Note: this confirms the signature is internally consistent with the
|
|
434
|
-
* embedded `publicKey`. It does NOT establish that `publicKey` is an
|
|
435
|
-
* authorized verification method for `subject` — that authorization check is
|
|
436
|
-
* the server's responsibility (it asserts the key is a current verification
|
|
437
|
-
* method on the subject's DID).
|
|
438
|
-
*/
|
|
439
|
-
static async verifyRecord(envelope) {
|
|
440
|
-
const signingInput = signedRecordSigningInput(envelope);
|
|
441
|
-
return SignatureService.verify(signingInput, envelope.signature, envelope.publicKey);
|
|
266
|
+
}, privateKey);
|
|
442
267
|
}
|
|
443
268
|
}
|
|
444
269
|
export default SignatureService;
|
package/dist/esm/index.js
CHANGED
|
@@ -64,8 +64,7 @@ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from '.
|
|
|
64
64
|
// Crypto / identity
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager.js';
|
|
67
|
-
export { SignatureService
|
|
68
|
-
export { canonicalize } from './crypto/canonicalJson.js';
|
|
67
|
+
export { SignatureService } from './crypto/signatureService.js';
|
|
69
68
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase.js';
|
|
70
69
|
// ---------------------------------------------------------------------------
|
|
71
70
|
// Devices
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
2
|
+
import { isWeb } from '../utils/platform.js';
|
|
3
|
+
import { logger } from '../utils/loggerUtils.js';
|
|
1
4
|
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
2
5
|
export function OxyServicesAccountsMixin(Base) {
|
|
3
6
|
return class extends Base {
|
|
@@ -40,6 +43,93 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
40
43
|
throw this.handleError(error);
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Switch the active session INTO a managed account.
|
|
48
|
+
*
|
|
49
|
+
* Calls `POST /accounts/:id/switch` with the signed-in operator's bearer.
|
|
50
|
+
* The server authorises the operator (must hold `account:act_as` over the
|
|
51
|
+
* target, directly or inherited — else 403; 404 if missing/archived; 403 if
|
|
52
|
+
* the target is a personal account), then mints a REAL session for the
|
|
53
|
+
* target account and returns it in the canonical login / `claimSessionByToken`
|
|
54
|
+
* shape (`{ sessionId, deviceId, expiresAt, accessToken, user, authuser }`).
|
|
55
|
+
*
|
|
56
|
+
* Unlike the removed `X-Acting-As` delegation header, the returned session
|
|
57
|
+
* IS the new identity: this plants `accessToken` as the active token —
|
|
58
|
+
* exactly like `claimSessionByToken` / `verifyChallenge` — so every
|
|
59
|
+
* subsequent request authenticates as the target account.
|
|
60
|
+
*
|
|
61
|
+
* Joining the device multi-account set (so the switch survives a reload and
|
|
62
|
+
* propagates cross-domain via `/auth/refresh-all`) requires a SECOND call, to
|
|
63
|
+
* `POST /auth/session`, made here after the token is planted. The switch route
|
|
64
|
+
* lives at `/accounts/*`, OUTSIDE the `oxy_rt_<authuser>` cookie's `Path=/auth`
|
|
65
|
+
* scope, so the server never sees the device's existing slots from it and
|
|
66
|
+
* would clobber slot 0 (destroying the operator's own session). `/auth/session`
|
|
67
|
+
* runs where those cookies ARE visible, so the server allocates a NEW slot that
|
|
68
|
+
* coexists with the operator's and returns its `authuser`. This step is
|
|
69
|
+
* web-only (native multi-account uses stored sessions, not cookies) and
|
|
70
|
+
* best-effort — a failure leaves the in-session switch intact; the switched
|
|
71
|
+
* account simply won't survive a reload until the cookie is next established.
|
|
72
|
+
*
|
|
73
|
+
* After planting, the SDK's identity-scoped GET cache is fully cleared so
|
|
74
|
+
* every cached read re-fetches as the new account. (The consuming
|
|
75
|
+
* `OxyContext` additionally invalidates its React Query cache and updates
|
|
76
|
+
* session state from the returned `user`; this clears the SDK's own HTTP
|
|
77
|
+
* cache at the source — `setTokens` deliberately preserves the warm cache for
|
|
78
|
+
* same-user silent refreshes, so the sweep here is explicit.)
|
|
79
|
+
*
|
|
80
|
+
* @param accountId - The target account's Mongo `_id`.
|
|
81
|
+
* @returns The minted session (planted) plus the device `authuser` slot.
|
|
82
|
+
*/
|
|
83
|
+
async switchToAccount(accountId) {
|
|
84
|
+
try {
|
|
85
|
+
const res = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/switch`, undefined, { cache: false });
|
|
86
|
+
// Plant the freshly minted session as the ACTIVE session, mirroring
|
|
87
|
+
// `claimSessionByToken` / `verifyChallenge`: the response body carries
|
|
88
|
+
// the first access token. The device refresh cookie is established below.
|
|
89
|
+
if (res?.accessToken) {
|
|
90
|
+
this.setTokens(res.accessToken);
|
|
91
|
+
}
|
|
92
|
+
// Register the switched session in the device's multi-account set by
|
|
93
|
+
// establishing its first-party refresh cookie. This MUST be a separate
|
|
94
|
+
// call to `POST /auth/session`: the switch route is at `/accounts/*`,
|
|
95
|
+
// outside the `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can
|
|
96
|
+
// never read the device's existing slots and would overwrite slot 0
|
|
97
|
+
// (destroying the operator's own session). `/auth/session` runs where the
|
|
98
|
+
// cookies ARE visible, so the server allocates a NEW slot that coexists
|
|
99
|
+
// with the operator's and returns its `authuser`. Web-only; best-effort.
|
|
100
|
+
let authuser = res.authuser;
|
|
101
|
+
if (isWeb()) {
|
|
102
|
+
try {
|
|
103
|
+
const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
|
|
104
|
+
if (typeof established?.authuser === 'number') {
|
|
105
|
+
authuser = established.authuser;
|
|
106
|
+
}
|
|
107
|
+
// /auth/session mints a fresh access token off the same session;
|
|
108
|
+
// re-plant it so the active token matches the rotated cookie.
|
|
109
|
+
if (established?.accessToken) {
|
|
110
|
+
this.setTokens(established.accessToken);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger.warn('[OxyServices] Failed to establish device refresh cookie after account switch; the switch is active in-session but may not survive a reload', { component: 'OxyServices', method: 'switchToAccount' }, error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Identity changed → drop the entire GET response cache so no entry
|
|
118
|
+
// personalised for the previous identity is reused. Cache keys are
|
|
119
|
+
// identity-scoped, so a different identity could not READ the old
|
|
120
|
+
// entries anyway, but clearing guarantees a clean refetch as the new
|
|
121
|
+
// account and frees the prior identity's resident data.
|
|
122
|
+
this.clearCache();
|
|
123
|
+
return {
|
|
124
|
+
...res,
|
|
125
|
+
...(typeof authuser === 'number' ? { authuser } : {}),
|
|
126
|
+
user: normalizeUserIdentity(res.user),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw this.handleError(error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
43
133
|
/**
|
|
44
134
|
* Create a new (non-personal) account. The caller becomes its `owner`.
|
|
45
135
|
* @param data - Account configuration: kind, optional parent, and profile.
|