@oxyhq/core 4.0.1 → 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.
Files changed (109) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +6 -18
  3. package/dist/cjs/OxyServices.base.js +0 -21
  4. package/dist/cjs/crypto/keyManager.js +7 -7
  5. package/dist/cjs/crypto/polyfill.js +6 -5
  6. package/dist/cjs/crypto/signatureService.js +44 -220
  7. package/dist/cjs/index.js +4 -8
  8. package/dist/cjs/mixins/OxyServices.accounts.js +54 -0
  9. package/dist/cjs/mixins/OxyServices.assets.js +2 -2
  10. package/dist/cjs/mixins/OxyServices.auth.js +3 -3
  11. package/dist/cjs/mixins/OxyServices.civic.js +3 -3
  12. package/dist/cjs/mixins/OxyServices.language.js +2 -2
  13. package/dist/cjs/mixins/OxyServices.utility.js +7 -95
  14. package/dist/cjs/utils/cacheKey.js +17 -19
  15. package/dist/cjs/utils/deviceManager.js +2 -2
  16. package/dist/cjs/utils/platform.js +0 -14
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/HttpService.js +6 -18
  19. package/dist/esm/OxyServices.base.js +0 -21
  20. package/dist/esm/crypto/keyManager.js +4 -4
  21. package/dist/esm/crypto/polyfill.js +5 -4
  22. package/dist/esm/crypto/signatureService.js +39 -214
  23. package/dist/esm/index.js +1 -2
  24. package/dist/esm/mixins/OxyServices.accounts.js +54 -0
  25. package/dist/esm/mixins/OxyServices.assets.js +1 -1
  26. package/dist/esm/mixins/OxyServices.auth.js +1 -1
  27. package/dist/esm/mixins/OxyServices.civic.js +3 -3
  28. package/dist/esm/mixins/OxyServices.language.js +1 -1
  29. package/dist/esm/mixins/OxyServices.utility.js +6 -94
  30. package/dist/esm/utils/cacheKey.js +17 -19
  31. package/dist/esm/utils/deviceManager.js +1 -1
  32. package/dist/esm/utils/platform.js +0 -12
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/HttpService.d.ts +3 -6
  35. package/dist/types/OxyServices.base.d.ts +0 -17
  36. package/dist/types/crypto/polyfill.d.ts +2 -2
  37. package/dist/types/crypto/signatureService.d.ts +18 -84
  38. package/dist/types/index.d.ts +3 -4
  39. package/dist/types/mixins/OxyServices.accounts.d.ts +57 -5
  40. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  41. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  42. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  43. package/dist/types/mixins/OxyServices.auth.d.ts +0 -2
  44. package/dist/types/mixins/OxyServices.civic.d.ts +3 -5
  45. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  46. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  47. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  48. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  49. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -2
  50. package/dist/types/mixins/OxyServices.identity.d.ts +8 -5
  51. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  52. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  53. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  54. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -44
  55. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  56. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  57. package/dist/types/mixins/OxyServices.redirect.d.ts +0 -2
  58. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  59. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  60. package/dist/types/mixins/OxyServices.silent.d.ts +0 -2
  61. package/dist/types/mixins/OxyServices.sso.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  64. package/dist/types/mixins/OxyServices.utility.d.ts +0 -32
  65. package/dist/types/server/auth.d.ts +0 -6
  66. package/dist/types/server/index.d.ts +1 -1
  67. package/dist/types/utils/cacheKey.d.ts +6 -7
  68. package/dist/types/utils/platform.d.ts +0 -8
  69. package/package.json +4 -7
  70. package/src/HttpService.ts +6 -22
  71. package/src/OxyServices.base.ts +0 -23
  72. package/src/__tests__/httpServiceCache.test.ts +0 -19
  73. package/src/crypto/__tests__/keyManager.atomicity.test.ts +2 -1
  74. package/src/crypto/__tests__/keyManager.test.ts +9 -7
  75. package/src/crypto/__tests__/signChallengeShared.test.ts +2 -1
  76. package/src/crypto/__tests__/signedRecord.test.ts +37 -150
  77. package/src/crypto/keyManager.ts +28 -17
  78. package/src/crypto/polyfill.ts +5 -4
  79. package/src/crypto/signatureService.ts +67 -255
  80. package/src/index.ts +3 -3
  81. package/src/mixins/OxyServices.accounts.ts +91 -3
  82. package/src/mixins/OxyServices.assets.ts +1 -1
  83. package/src/mixins/OxyServices.auth.ts +1 -1
  84. package/src/mixins/OxyServices.civic.ts +6 -17
  85. package/src/mixins/OxyServices.identity.ts +8 -2
  86. package/src/mixins/OxyServices.language.ts +1 -1
  87. package/src/mixins/OxyServices.nodes.ts +1 -12
  88. package/src/mixins/OxyServices.utility.ts +6 -119
  89. package/src/mixins/__tests__/OxyServices.civic.test.ts +2 -2
  90. package/src/mixins/__tests__/accounts.test.ts +70 -0
  91. package/src/server/auth.ts +0 -7
  92. package/src/server/index.ts +0 -1
  93. package/src/utils/__tests__/cacheKey.test.ts +0 -0
  94. package/src/utils/cacheKey.ts +16 -21
  95. package/src/utils/deviceManager.ts +1 -1
  96. package/src/utils/platform.ts +0 -14
  97. package/dist/cjs/crypto/canonicalJson.js +0 -107
  98. package/dist/cjs/utils/platformCrypto.js +0 -165
  99. package/dist/cjs/utils/platformCrypto.native.js +0 -123
  100. package/dist/esm/crypto/canonicalJson.js +0 -104
  101. package/dist/esm/utils/platformCrypto.js +0 -125
  102. package/dist/esm/utils/platformCrypto.native.js +0 -80
  103. package/dist/types/crypto/canonicalJson.d.ts +0 -44
  104. package/dist/types/utils/platformCrypto.d.ts +0 -87
  105. package/dist/types/utils/platformCrypto.native.d.ts +0 -54
  106. package/src/crypto/__tests__/canonicalJson.test.ts +0 -116
  107. package/src/crypto/canonicalJson.ts +0 -120
  108. package/src/utils/platformCrypto.native.ts +0 -101
  109. package/src/utils/platformCrypto.ts +0 -145
@@ -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, isReactNative, getPlatformOS } from './utils/platform.js';
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 and acting-as id. See that
634
- * function's docs for the full resolution contract (anon fallback, decoded
635
- * `userId || id`, token-hash fallback for undecodable tokens).
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(), this._actingAsUserId);
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, isReactNative, isNodeJS } from '../utils/platform.js';
10
- import { loadExpoCrypto, loadNodeCrypto, loadSecureStore } from '../utils/platformCrypto.js';
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 `platformCrypto`, which is a per-platform module
72
- * (`platformCrypto.ts` vs `platformCrypto.react-native.ts`) selected by the
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 `platformCrypto` module — see that file's doc-comment for
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 '../utils/platformCrypto.js';
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
- * `platformCrypto`, so available without any async warm-up). On Node /
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 using ECDSA secp256k1.
5
- * Used for authenticating requests and proving identity ownership.
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 _cjs_elliptic from 'elliptic';
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 keyPair = await KeyManager.getKeyPairObject();
124
- if (!keyPair) {
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
- const messageHash = await sha256(message);
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 SignatureService.verify(messageWithTimestamp, signature, publicKey);
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 `signWithKey`. Used by "Sign in with Oxy"
251
- * same-device shared-keychain SSO (Mechanism A): a sibling native app proves
252
- * control of the shared identity to mint its own session.
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 SignatureService.signWithKey(message, privateKey);
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 SignatureService.verify(message, signature, publicKey);
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` (see {@link signedRecordSigningInput}); `alg` is
332
- * `ES256K-DER-SHA256` (secp256k1 over the SHA-256 of the canonical bytes,
333
- * DER-encoded), the same scheme this service uses everywhere else.
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 publicKey = await KeyManager.getPublicKey();
343
- if (!publicKey) {
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
- const version = 1;
347
- const issuer = subject;
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
- publicKey,
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 over {@link signedRecordSigningInput}) but
376
- * `version` is `2` and the signed bytes additionally cover the chain fields:
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 publicKey = await KeyManager.getPublicKey();
392
- if (!publicKey) {
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
- const signingInput = signedRecordSigningInput({
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, signedRecordSigningInput, computeRecordId } from './crypto/signatureService.js';
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,4 @@
1
+ import { normalizeUserIdentity } from '../utils/userIdentity.js';
1
2
  import { CACHE_TIMES } from './mixinHelpers.js';
2
3
  export function OxyServicesAccountsMixin(Base) {
3
4
  return class extends Base {
@@ -40,6 +41,59 @@ export function OxyServicesAccountsMixin(Base) {
40
41
  throw this.handleError(error);
41
42
  }
42
43
  }
44
+ /**
45
+ * Switch the active session INTO a managed account.
46
+ *
47
+ * Calls `POST /accounts/:id/switch` with the signed-in operator's bearer.
48
+ * The server authorises the operator (must hold `account:act_as` over the
49
+ * target, directly or inherited — else 403; 404 if missing/archived; 403 if
50
+ * the target is a personal account), then mints a REAL session for the
51
+ * target account and returns it in the canonical login / `claimSessionByToken`
52
+ * shape (`{ sessionId, deviceId, expiresAt, accessToken, user, authuser }`).
53
+ *
54
+ * Unlike the removed `X-Acting-As` delegation header, the returned session
55
+ * IS the new identity: this plants `accessToken` as the active token —
56
+ * exactly like `claimSessionByToken` / `verifyChallenge` — so every
57
+ * subsequent request authenticates as the target account. The refresh token
58
+ * is the server-set httpOnly `oxy_rt_<authuser>` cookie (never in the body),
59
+ * so it joins the device multi-account set and survives reload /
60
+ * `refresh-all` with no extra client work.
61
+ *
62
+ * After planting, the SDK's identity-scoped GET cache is fully cleared so
63
+ * every cached read re-fetches as the new account. (The consuming
64
+ * `OxyContext` additionally invalidates its React Query cache and updates
65
+ * session state from the returned `user`; this clears the SDK's own HTTP
66
+ * cache at the source — `setTokens` deliberately preserves the warm cache for
67
+ * same-user silent refreshes, so the sweep here is explicit.)
68
+ *
69
+ * @param accountId - The target account's Mongo `_id`.
70
+ * @returns The minted session (planted) plus the device `authuser` slot.
71
+ */
72
+ async switchToAccount(accountId) {
73
+ try {
74
+ const res = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/switch`, undefined, { cache: false });
75
+ // Plant the freshly minted session as the ACTIVE session, mirroring
76
+ // `claimSessionByToken` / `verifyChallenge`: the response body carries
77
+ // the first access token; the refresh token is the server-set httpOnly
78
+ // cookie, so there is nothing else to store here.
79
+ if (res?.accessToken) {
80
+ this.setTokens(res.accessToken);
81
+ }
82
+ // Identity changed → drop the entire GET response cache so no entry
83
+ // personalised for the previous identity is reused. Cache keys are
84
+ // identity-scoped, so a different identity could not READ the old
85
+ // entries anyway, but clearing guarantees a clean refetch as the new
86
+ // account and frees the prior identity's resident data.
87
+ this.clearCache();
88
+ return {
89
+ ...res,
90
+ user: normalizeUserIdentity(res.user),
91
+ };
92
+ }
93
+ catch (error) {
94
+ throw this.handleError(error);
95
+ }
96
+ }
43
97
  /**
44
98
  * Create a new (non-personal) account. The caller becomes its `owner`.
45
99
  * @param data - Account configuration: kind, optional parent, and profile.
@@ -1,4 +1,4 @@
1
- import { isReactNative } from '../utils/platform.js';
1
+ import { isReactNative } from '@oxyhq/protocol';
2
2
  export function OxyServicesAssetsMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,7 +1,7 @@
1
1
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  import { KeyManager } from '../crypto/keyManager.js';
3
3
  import { SignatureService } from '../crypto/signatureService.js';
4
- import { loadNodeCrypto } from '../utils/platformCrypto.js';
4
+ import { loadNodeCrypto } from '@oxyhq/protocol';
5
5
  import { logger } from '../utils/loggerUtils.js';
6
6
  import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { canonicalize } from '../crypto/canonicalJson.js';
1
+ import { canonicalize, verifySignature } from '@oxyhq/protocol';
2
2
  import { SignatureService } from '../crypto/signatureService.js';
3
3
  import { buildUserDid } from './OxyServices.identity.js';
4
4
  import { CACHE_TIMES } from './mixinHelpers.js';
@@ -150,7 +150,7 @@ export function parseAttestPayload(raw) {
150
150
  * key, matching the server which omits absent keys entirely) and checks the
151
151
  * `ES256K-DER-SHA256` signature against `attestation.publicKey`.
152
152
  *
153
- * NEVER throws: `SignatureService.verify` already swallows malformed-input
153
+ * NEVER throws: `verifySignature` already swallows malformed-input
154
154
  * errors and returns `false`, and an absent attestation short-circuits to
155
155
  * `false`. A pure, reusable helper (Commons can call it on a cached card).
156
156
  *
@@ -165,7 +165,7 @@ export async function verifyPublicCardAttestation(card, attestation) {
165
165
  if (!signature || !publicKey) {
166
166
  return false;
167
167
  }
168
- return SignatureService.verify(canonicalize(card), signature, publicKey);
168
+ return verifySignature(canonicalize(card), signature, publicKey);
169
169
  }
170
170
  export function OxyServicesCivicMixin(Base) {
171
171
  return class extends Base {
@@ -2,7 +2,7 @@
2
2
  * Language Methods Mixin
3
3
  */
4
4
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
5
- import { loadAsyncStorage } from '../utils/platformCrypto.js';
5
+ import { loadAsyncStorage } from '@oxyhq/protocol';
6
6
  import { isDev } from '../shared/utils/debugUtils.js';
7
7
  export function OxyServicesLanguageMixin(Base) {
8
8
  return class extends Base {