@oxyhq/core 3.10.0 → 3.11.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 (105) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +9 -2
  3. package/dist/cjs/HttpService.js +27 -9
  4. package/dist/cjs/OxyServices.base.js +3 -2
  5. package/dist/cjs/crypto/canonicalJson.js +107 -0
  6. package/dist/cjs/crypto/keyManager.js +67 -8
  7. package/dist/cjs/crypto/signatureService.js +103 -0
  8. package/dist/cjs/i18n/locales/en-US.json +9 -0
  9. package/dist/cjs/i18n/locales/es-ES.json +9 -0
  10. package/dist/cjs/i18n/locales/locales/en-US.json +9 -0
  11. package/dist/cjs/i18n/locales/locales/es-ES.json +9 -0
  12. package/dist/cjs/index.js +15 -5
  13. package/dist/cjs/mixins/OxyServices.assets.js +45 -7
  14. package/dist/cjs/mixins/OxyServices.auth.js +190 -1
  15. package/dist/cjs/mixins/OxyServices.identity.js +291 -0
  16. package/dist/cjs/mixins/OxyServices.sso.js +28 -1
  17. package/dist/cjs/mixins/OxyServices.user.js +1 -0
  18. package/dist/cjs/mixins/OxyServices.utility.js +52 -23
  19. package/dist/cjs/mixins/index.js +3 -0
  20. package/dist/cjs/server/cors.js +20 -21
  21. package/dist/cjs/server/rateLimit.js +32 -8
  22. package/dist/cjs/utils/fapiAutoDetect.js +12 -42
  23. package/dist/cjs/utils/ssoReturn.js +1 -1
  24. package/dist/esm/.tsbuildinfo +1 -1
  25. package/dist/esm/AuthManager.js +9 -2
  26. package/dist/esm/HttpService.js +27 -9
  27. package/dist/esm/OxyServices.base.js +3 -2
  28. package/dist/esm/crypto/canonicalJson.js +104 -0
  29. package/dist/esm/crypto/keyManager.js +67 -8
  30. package/dist/esm/crypto/signatureService.js +102 -0
  31. package/dist/esm/i18n/locales/en-US.json +9 -0
  32. package/dist/esm/i18n/locales/es-ES.json +9 -0
  33. package/dist/esm/i18n/locales/locales/en-US.json +9 -0
  34. package/dist/esm/i18n/locales/locales/es-ES.json +9 -0
  35. package/dist/esm/index.js +10 -2
  36. package/dist/esm/mixins/OxyServices.assets.js +45 -7
  37. package/dist/esm/mixins/OxyServices.auth.js +190 -1
  38. package/dist/esm/mixins/OxyServices.identity.js +287 -0
  39. package/dist/esm/mixins/OxyServices.sso.js +28 -1
  40. package/dist/esm/mixins/OxyServices.user.js +1 -0
  41. package/dist/esm/mixins/OxyServices.utility.js +52 -23
  42. package/dist/esm/mixins/index.js +3 -0
  43. package/dist/esm/server/cors.js +20 -21
  44. package/dist/esm/server/rateLimit.js +32 -8
  45. package/dist/esm/utils/fapiAutoDetect.js +12 -41
  46. package/dist/esm/utils/ssoReturn.js +1 -1
  47. package/dist/types/.tsbuildinfo +1 -1
  48. package/dist/types/HttpService.d.ts +3 -0
  49. package/dist/types/OxyServices.d.ts +2 -2
  50. package/dist/types/crypto/canonicalJson.d.ts +44 -0
  51. package/dist/types/crypto/keyManager.d.ts +7 -0
  52. package/dist/types/crypto/signatureService.d.ts +61 -0
  53. package/dist/types/index.d.ts +7 -3
  54. package/dist/types/mixins/OxyServices.assets.d.ts +6 -1
  55. package/dist/types/mixins/OxyServices.auth.d.ts +136 -0
  56. package/dist/types/mixins/OxyServices.identity.d.ts +249 -0
  57. package/dist/types/mixins/OxyServices.sso.d.ts +4 -1
  58. package/dist/types/mixins/OxyServices.utility.d.ts +3 -3
  59. package/dist/types/mixins/index.d.ts +2 -1
  60. package/dist/types/models/interfaces.d.ts +3 -0
  61. package/dist/types/server/cors.d.ts +5 -5
  62. package/dist/types/utils/fapiAutoDetect.d.ts +6 -23
  63. package/dist/types/utils/ssoReturn.d.ts +1 -1
  64. package/package.json +3 -2
  65. package/src/AuthManager.ts +8 -2
  66. package/src/HttpService.ts +36 -8
  67. package/src/OxyServices.base.ts +3 -2
  68. package/src/OxyServices.ts +1 -1
  69. package/src/__tests__/authManager.security.test.ts +31 -0
  70. package/src/__tests__/authSocket.test.ts +96 -0
  71. package/src/__tests__/httpServiceCsrf.test.ts +75 -0
  72. package/src/crypto/__tests__/canonicalJson.test.ts +116 -0
  73. package/src/crypto/__tests__/keyManager.atomicity.test.ts +41 -2
  74. package/src/crypto/__tests__/signChallengeShared.test.ts +64 -0
  75. package/src/crypto/__tests__/signedRecord.test.ts +125 -0
  76. package/src/crypto/canonicalJson.ts +120 -0
  77. package/src/crypto/keyManager.ts +62 -12
  78. package/src/crypto/signatureService.ts +126 -0
  79. package/src/i18n/locales/en-US.json +9 -0
  80. package/src/i18n/locales/es-ES.json +9 -0
  81. package/src/index.ts +28 -3
  82. package/src/mixins/OxyServices.assets.ts +56 -7
  83. package/src/mixins/OxyServices.auth.ts +309 -1
  84. package/src/mixins/OxyServices.identity.ts +445 -0
  85. package/src/mixins/OxyServices.sso.ts +30 -1
  86. package/src/mixins/OxyServices.user.ts +1 -0
  87. package/src/mixins/OxyServices.utility.ts +57 -23
  88. package/src/mixins/__tests__/OxyServices.identity.test.ts +364 -0
  89. package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
  90. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  91. package/src/mixins/__tests__/commonsSignIn.test.ts +277 -0
  92. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +13 -0
  93. package/src/mixins/__tests__/serviceAuth.test.ts +49 -2
  94. package/src/mixins/__tests__/sso.test.ts +31 -0
  95. package/src/mixins/index.ts +4 -0
  96. package/src/models/interfaces.ts +3 -0
  97. package/src/server/__tests__/cors.test.ts +5 -1
  98. package/src/server/__tests__/rateLimit.test.ts +116 -0
  99. package/src/server/cors.ts +25 -20
  100. package/src/server/rateLimit.ts +39 -8
  101. package/src/utils/__tests__/consumeSsoReturn.test.ts +1 -1
  102. package/src/utils/__tests__/fapiAutoDetect.test.ts +40 -11
  103. package/src/utils/__tests__/ssoReturn.test.ts +1 -1
  104. package/src/utils/fapiAutoDetect.ts +12 -39
  105. package/src/utils/ssoReturn.ts +2 -2
@@ -25,6 +25,7 @@ export interface RequestOptions {
25
25
  timeout?: number;
26
26
  signal?: AbortSignal;
27
27
  headers?: Record<string, string>;
28
+ responseType?: 'blob';
28
29
  }
29
30
  interface RequestConfig extends RequestOptions {
30
31
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -183,6 +184,8 @@ export declare class HttpService {
183
184
  * Build full URL with query params
184
185
  */
185
186
  private buildURL;
187
+ private getCredentialsMode;
188
+ private shouldSendCredentials;
186
189
  /**
187
190
  * Fetch CSRF token from server (with deduplication)
188
191
  * Required for state-changing requests (POST, PUT, PATCH, DELETE)
@@ -122,7 +122,7 @@ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxySe
122
122
  waitForIframeAuth(iframe: HTMLIFrameElement, timeout: number, expectedOrigin: string): Promise<SessionLoginResponse | null>;
123
123
  signInWithRedirect(options?: RedirectAuthOptions): void;
124
124
  signUpWithRedirect(options?: RedirectAuthOptions): void;
125
- exchangeSsoCode(code: string): Promise<SessionLoginResponse>;
125
+ exchangeSsoCode(code: string, state?: string): Promise<SessionLoginResponse>;
126
126
  generateSsoState(): string;
127
127
  auth(options?: {
128
128
  debug?: boolean;
@@ -153,7 +153,7 @@ export declare const OXY_CLOUD_URL = "https://cloud.oxy.so";
153
153
  /**
154
154
  * Export the default Oxy API URL (for documentation)
155
155
  */
156
- export declare const OXY_API_URL: any;
156
+ export declare const OXY_API_URL: string;
157
157
  /**
158
158
  * Pre-configured client instance for easy import
159
159
  * Uses OXY_API_URL as baseURL and OXY_CLOUD_URL as cloudURL
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Canonical JSON (RFC 8785 / JCS-style) serialization.
3
+ *
4
+ * `canonicalize(value)` produces a deterministic string for any JSON-compatible
5
+ * value so that a client which SIGNS a record and a server which VERIFIES it
6
+ * agree byte-for-byte on the signing input — regardless of the order in which
7
+ * object keys happen to be written, how the value was deserialized, or which
8
+ * runtime built it.
9
+ *
10
+ * This is the load-bearing primitive for the self-sovereign identity layer's
11
+ * signed records (`SignatureService.signRecord` + the API's record-verify path):
12
+ * both sides import THIS function from `@oxyhq/core`, so cross-implementation
13
+ * number/string formatting differences cannot cause a verify mismatch.
14
+ *
15
+ * Rules (the JSON Canonicalization Scheme subset we need):
16
+ * - Objects: keys are sorted (ascending, by UTF-16 code unit — the default
17
+ * `Array.prototype.sort` order) and serialized recursively. Properties whose
18
+ * value is `undefined`, a function, or a symbol are OMITTED (matching
19
+ * `JSON.stringify` object semantics).
20
+ * - Arrays: element order is PRESERVED; `undefined`/function/symbol elements
21
+ * serialize to `null` (matching `JSON.stringify` array semantics).
22
+ * - `null`, booleans, strings, and finite numbers serialize via the standard
23
+ * JSON representation.
24
+ * - Values exposing a `toJSON()` method (e.g. `Date`) are replaced by its
25
+ * result first, then serialized — so a `Date` and its ISO-string equivalent
26
+ * canonicalize identically (the wire always carries the string form).
27
+ * - Non-finite numbers (`NaN`, `Infinity`) and `bigint` are not part of the
28
+ * JSON data model and throw, rather than silently producing `null`.
29
+ *
30
+ * Platform-agnostic — zero dependencies, no `require()`, no react/react-native/
31
+ * expo. Safe in the dual CJS + ESM build.
32
+ */
33
+ /**
34
+ * Produce the canonical JSON string for `value`.
35
+ *
36
+ * Deterministic: two structurally-equal values yield identical strings even if
37
+ * their object keys were written in different orders. Use this — never an
38
+ * ad-hoc `JSON.stringify` of a hand-sorted object — as the signing input for
39
+ * signed records, so client signing and server verification cannot drift.
40
+ *
41
+ * @throws if `value` (or any nested member used as the top-level/primitive)
42
+ * contains a non-finite number or a `bigint`, which have no JSON form.
43
+ */
44
+ export declare function canonicalize(value: unknown): string;
@@ -186,6 +186,13 @@ export declare class KeyManager {
186
186
  * @internal
187
187
  */
188
188
  private static _persistIdentityAtomic;
189
+ /**
190
+ * Restore the backup slot to a previously-snapshotted state. Best-effort so
191
+ * the original persistence error remains the one surfaced to the caller.
192
+ *
193
+ * @internal
194
+ */
195
+ private static _rollbackBackup;
189
196
  /**
190
197
  * Restore the primary slot to a previously-snapshotted (privA, pubA) pair,
191
198
  * or delete it entirely if there was no prior identity. Best-effort: every
@@ -4,6 +4,23 @@
4
4
  * Handles signing and verification of messages using ECDSA secp256k1.
5
5
  * Used for authenticating requests and proving identity ownership.
6
6
  */
7
+ import type { SignedRecordEnvelope } from '@oxyhq/contracts';
8
+ /**
9
+ * The signing-input portion of a {@link SignedRecordEnvelope}: every field
10
+ * EXCEPT the `publicKey` and `signature`. Both the client (when signing) and
11
+ * the server (when verifying) canonicalize exactly these fields, so they agree
12
+ * on the bytes that the signature covers.
13
+ */
14
+ export type SignedRecordSigningFields = Pick<SignedRecordEnvelope, 'version' | 'type' | 'subject' | 'issuer' | 'record' | 'issuedAt'>;
15
+ /**
16
+ * Compute the canonical signing input for a signed-record envelope.
17
+ *
18
+ * This is the single definition of "what the signature covers": the canonical
19
+ * JSON of `{version, type, subject, issuer, record, issuedAt}`. `@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
+ export declare function signedRecordSigningInput(fields: SignedRecordSigningFields): string;
7
24
  export interface SignedMessage {
8
25
  message: string;
9
26
  signature: string;
@@ -63,6 +80,22 @@ export declare class SignatureService {
63
80
  * Used for challenge-response authentication
64
81
  */
65
82
  static signChallenge(challenge: string): Promise<AuthChallenge>;
83
+ /**
84
+ * Create a signed authentication challenge response using the SHARED identity
85
+ * key (the cross-app `group.so.oxy.shared` keychain key), not the primary
86
+ * device key.
87
+ *
88
+ * Mirrors {@link signChallenge} exactly — same message format
89
+ * (`auth:${publicKey}:${challenge}:${timestamp}`) so the server verification
90
+ * path is unchanged — but sources the shared public/private key from
91
+ * `KeyManager` and signs with `signWithKey`. Used by "Sign in with Oxy"
92
+ * same-device shared-keychain SSO (Mechanism A): a sibling native app proves
93
+ * control of the shared identity to mint its own session.
94
+ *
95
+ * Throws if no shared identity exists (native-only; the shared keychain is
96
+ * unavailable on web).
97
+ */
98
+ static signChallengeWithSharedKey(challenge: string): Promise<AuthChallenge>;
66
99
  /**
67
100
  * Verify a challenge response
68
101
  */
@@ -86,5 +119,33 @@ export declare class SignatureService {
86
119
  publicKey: string;
87
120
  timestamp: number;
88
121
  }>;
122
+ /**
123
+ * Build a signed-record envelope for a self-issued identity/profile record.
124
+ *
125
+ * The envelope is self-issued: `issuer` equals `subject` (the signer's DID).
126
+ * The signature covers the canonical JSON of every field EXCEPT `publicKey`
127
+ * and `signature` (see {@link signedRecordSigningInput}); `alg` is
128
+ * `ES256K-DER-SHA256` (secp256k1 over the SHA-256 of the canonical bytes,
129
+ * DER-encoded), the same scheme this service uses everywhere else.
130
+ *
131
+ * Requires a stored identity (native secure storage); throws if none exists.
132
+ *
133
+ * @param type - The record category (`'identity'` or `'profile'`).
134
+ * @param subject - The subject DID the record is about (also the issuer).
135
+ * @param record - The arbitrary record payload to attest to.
136
+ */
137
+ static signRecord(type: SignedRecordEnvelope['type'], subject: string, record: Record<string, unknown>): Promise<SignedRecordEnvelope>;
138
+ /**
139
+ * Verify a signed-record envelope: recompute the canonical signing input from
140
+ * the envelope's own fields and check the signature against the envelope's
141
+ * `publicKey`.
142
+ *
143
+ * Note: this confirms the signature is internally consistent with the
144
+ * embedded `publicKey`. It does NOT establish that `publicKey` is an
145
+ * authorized verification method for `subject` — that authorization check is
146
+ * the server's responsibility (it asserts the key is a current verification
147
+ * method on the subject's DID).
148
+ */
149
+ static verifyRecord(envelope: SignedRecordEnvelope): Promise<boolean>;
89
150
  }
90
151
  export default SignatureService;
@@ -30,6 +30,7 @@ export type { SilentAuthOptions } from './mixins/OxyServices.silent';
30
30
  export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
31
31
  export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
32
32
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
33
+ export type { CommonsSignInHandle, CommonsSignInStatus, CommonsApprovalInfo, CommonsSignInActionResult, } from './mixins/OxyServices.auth';
33
34
  export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
34
35
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount, } from './mixins/OxyServices.managedAccounts';
35
36
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
@@ -41,6 +42,8 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
41
42
  export type { Application, PublicApplication, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
42
43
  export type { Workspace, WorkspaceMember, WorkspaceRole, WorkspaceType, WorkspaceStatus, WorkspaceMemberStatus, CreateWorkspaceInput, UpdateWorkspaceInput, InviteWorkspaceMemberInput, UpdateWorkspaceMemberInput, TransferWorkspaceOwnershipInput, WorkspaceSuccessResult, } from './mixins/OxyServices.workspaces';
43
44
  export type { ReputationCategory, TrustTier, ReputationTransactionStatus, ReputationTargetEntityType, ReputationDisputeStatus, ReputationInfluenceContext, ReputationTransaction, ReputationBalanceBreakdown, ReputationInfluence, ReputationReliability, ReputationBalance, ReputationDispute, ReputationRule, ReputationLeaderboardEntry, ReputationInfluenceResult, ReverseReputationTransactionResult, AwardReputationInput, CreateReputationDisputeInput, ResolveReputationDisputeInput, UpsertReputationRuleInput, ReverseReputationTransactionInput, } from './mixins/OxyServices.reputation';
45
+ export { buildUserDid } from './mixins/OxyServices.identity';
46
+ export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, } from './mixins/OxyServices.identity';
44
47
  export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
45
48
  export type { HandleApiErrorOptions } from './utils/authHelpers';
46
49
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
@@ -48,8 +51,9 @@ export type { ClientSession, StorageKeys, MinimalUserData, SessionLoginResponse,
48
51
  export type { RefreshAllResponse, RefreshAllAccount, RefreshAllAccountUser, RefreshCookieResponse, } from './models/interfaces';
49
52
  export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager';
50
53
  export type { KeyPair } from './crypto/keyManager';
51
- export { SignatureService } from './crypto/signatureService';
52
- export type { SignedMessage, AuthChallenge } from './crypto/signatureService';
54
+ export { SignatureService, signedRecordSigningInput } from './crypto/signatureService';
55
+ export type { SignedMessage, AuthChallenge, SignedRecordSigningFields } from './crypto/signatureService';
56
+ export { canonicalize } from './crypto/canonicalJson';
53
57
  export { RecoveryPhraseService } from './crypto/recoveryPhrase';
54
58
  export type { RecoveryPhraseResult } from './crypto/recoveryPhrase';
55
59
  export { DeviceManager } from './utils/deviceManager';
@@ -80,7 +84,7 @@ export type { LogContext } from './utils/loggerUtils';
80
84
  export { updateAvatarVisibility } from './utils/avatarUtils';
81
85
  export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, mergeAccountsFromRefreshAll, getAccountColor, } from './utils/accountUtils';
82
86
  export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
83
- export { autoDetectAuthWebUrl, registrableApex, MULTIPART_TLDS } from './utils/fapiAutoDetect';
87
+ export { autoDetectAuthWebUrl, registrableApex } from './utils/fapiAutoDetect';
84
88
  export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './utils/authWebUrl';
85
89
  export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn';
86
90
  export type { SsoReturnKind, SsoReturnResult, ConsumeSsoReturnDeps } from './utils/ssoReturn';
@@ -1,5 +1,9 @@
1
1
  import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant } from '../models/interfaces';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
+ interface FileDownloadUrlOptions {
4
+ /** Omit bearer access tokens from generated URLs, even when authenticated. */
5
+ omitToken?: boolean;
6
+ }
3
7
  export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T): {
4
8
  new (...args: any[]): {
5
9
  /**
@@ -32,7 +36,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
32
36
  *
33
37
  * For a CDN-signed URL fetched from the API, use {@link getFileDownloadUrlAsync}.
34
38
  */
35
- getFileDownloadUrl(fileId: string, variant?: string, expiresIn?: number): string;
39
+ getFileDownloadUrl(fileId: string, variant?: string, expiresIn?: number, options?: FileDownloadUrlOptions): string;
36
40
  /**
37
41
  * Get file download URL asynchronously (returns signed URL directly from CDN)
38
42
  */
@@ -168,3 +172,4 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
168
172
  }>;
169
173
  };
170
174
  } & T;
175
+ export {};
@@ -6,6 +6,7 @@
6
6
  import type { User, RefreshAllResponse, RefreshCookieResponse } from '../models/interfaces';
7
7
  import type { SessionLoginResponse } from '../models/session';
8
8
  import type { OxyServicesBase } from '../OxyServices.base';
9
+ import type { PublicApplication } from './OxyServices.applications';
9
10
  export interface ChallengeResponse {
10
11
  challenge: string;
11
12
  expiresAt: string;
@@ -48,6 +49,60 @@ export interface PublicKeyCheckResponse {
48
49
  registered: boolean;
49
50
  message: string;
50
51
  }
52
+ /**
53
+ * Handle returned by {@link OxyServicesAuthMixin.startCommonsSignIn} for a
54
+ * relying-party app initiating a "Sign in with Oxy" flow.
55
+ *
56
+ * `sessionToken` is the SECRET, high-entropy device-flow credential — it stays
57
+ * on the initiating client, is exchanged once via `claimSessionByToken`, and is
58
+ * NEVER placed in the QR/deep-link. `authorizeCode` is the PUBLIC handle carried
59
+ * in `qrPayload`; the approver (Commons) resolves it via
60
+ * {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
61
+ */
62
+ export interface CommonsSignInHandle {
63
+ /** Secret device-flow token (held by the initiator; exchanged via `claimSessionByToken`). */
64
+ sessionToken: string;
65
+ /** Public, single-use authorize code carried in the QR / deep-link. */
66
+ authorizeCode: string;
67
+ /** Ready-to-render deep-link / universal-link string (`oxycommons://approve?...`). */
68
+ qrPayload: string;
69
+ /** Server-authoritative expiry (epoch milliseconds). */
70
+ expiresAt: number;
71
+ /** Session lifecycle status as reported by the server (e.g. `'pending'`). */
72
+ status: string;
73
+ }
74
+ /** Poll result for a "Sign in with Oxy" device-flow session (`GET /auth/session/status`). */
75
+ export interface CommonsSignInStatus {
76
+ /** True once an approver has authorized the session. */
77
+ authorized: boolean;
78
+ /** The authorized session id (present once `authorized`). */
79
+ sessionId?: string;
80
+ /** The approving identity's public key (present once `authorized`). */
81
+ publicKey?: string;
82
+ /** Lifecycle status (`'pending'` | `'authorized'` | `'cancelled'` | `'expired'`). */
83
+ status?: string;
84
+ }
85
+ /**
86
+ * Server-resolved approval context shown by the approver (Commons) before
87
+ * authorizing — the TRUSTED identity of the requesting app, resolved from the
88
+ * `authorizeCode` server-side (never from the QR string).
89
+ */
90
+ export interface CommonsApprovalInfo {
91
+ /** Sanitized, display-safe identity of the requesting application. */
92
+ application: PublicApplication;
93
+ /** OAuth scopes the application is requesting. */
94
+ scopes: string[];
95
+ /** The origin the session is bound to (the RP web origin), when applicable. */
96
+ boundOrigin?: string;
97
+ /** Server-authoritative expiry (epoch milliseconds). */
98
+ expiresAt: number;
99
+ /** Session lifecycle status. */
100
+ status: string;
101
+ }
102
+ /** Result of approving / denying a "Sign in with Oxy" request. */
103
+ export interface CommonsSignInActionResult {
104
+ success: boolean;
105
+ }
51
106
  export interface ServiceTokenResponse {
52
107
  token: string;
53
108
  expiresIn: number;
@@ -275,6 +330,87 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
275
330
  expiresAt: string;
276
331
  user: User;
277
332
  }>;
333
+ /**
334
+ * MECHANISM A — same-device shared-keychain SSO.
335
+ *
336
+ * Native-only. If this device holds a shared identity (the cross-app
337
+ * `group.so.oxy.shared` keychain key), prove control of it and mint a
338
+ * session: `requestChallenge(sharedPublicKey)` → `signChallengeWithSharedKey`
339
+ * → `verifyChallenge` (which plants the tokens). Returns `null` on web or
340
+ * when no shared identity is present — never throws for the absent-identity
341
+ * case, so a cold-boot caller can fall through to the next step.
342
+ *
343
+ * The cold-boot wiring that CALLS this lives in `OxyContext`
344
+ * (`@oxyhq/services`); this method just performs the exchange.
345
+ */
346
+ signInWithSharedIdentity(opts?: {
347
+ deviceName?: string;
348
+ deviceFingerprint?: string;
349
+ }): Promise<SessionLoginResponse | null>;
350
+ /**
351
+ * MECHANISM B (relying party) — begin a "Sign in with Oxy" handoff.
352
+ *
353
+ * Generates a secret device-flow `sessionToken` client-side (it never
354
+ * appears in the QR), registers it with `POST /auth/session/create`, and
355
+ * returns the server-issued public `authorizeCode` + ready-to-render
356
+ * `qrPayload`. Render the QR (web) / open the deep-link (same-device); the
357
+ * approver resolves the code and authorizes. Then poll with
358
+ * {@link pollCommonsSignIn} and, on `authorized`, exchange the
359
+ * `sessionToken` via the existing `claimSessionByToken`.
360
+ *
361
+ * @param params.clientId - The RP's registered OAuth client id
362
+ * (ApplicationCredential publicKey); required so the server can resolve the
363
+ * requesting application's identity.
364
+ */
365
+ startCommonsSignIn(params: {
366
+ clientId: string;
367
+ }): Promise<CommonsSignInHandle>;
368
+ /**
369
+ * MECHANISM B (relying party) — poll a device-flow session for approval.
370
+ *
371
+ * Backstop for the auth socket. On `authorized` (with a `sessionId`), the
372
+ * caller exchanges the secret `sessionToken` via the existing
373
+ * `claimSessionByToken` to mint the first access token.
374
+ *
375
+ * @param sessionToken - The secret token from {@link startCommonsSignIn}.
376
+ */
377
+ pollCommonsSignIn(sessionToken: string): Promise<CommonsSignInStatus>;
378
+ /**
379
+ * MECHANISM B (approver / Commons) — resolve the TRUSTED identity of a
380
+ * sign-in request from its public `authorizeCode`.
381
+ *
382
+ * The returned `application` is resolved server-side and is the only safe
383
+ * thing to display in the approval UI — NEVER trust the app/name/origin
384
+ * strings carried in the QR payload. Public (no auth required).
385
+ *
386
+ * @param authorizeCode - The public code scanned from the QR / deep-link.
387
+ */
388
+ getCommonsApprovalInfo(authorizeCode: string): Promise<CommonsApprovalInfo>;
389
+ /**
390
+ * MECHANISM B (approver / Commons) — approve a sign-in request by signing a
391
+ * fresh challenge with the PRIMARY local identity key.
392
+ *
393
+ * Commons holds the user's identity as its primary key (not the shared
394
+ * key), so this uses `signChallenge`. The signed-but-cookieless authorize
395
+ * endpoint resolves the user from the verified signer — the RP that started
396
+ * the flow then claims its session. Native-only (requires a local identity).
397
+ *
398
+ * @param params.authorizeCode - The public code being approved.
399
+ * @param params.deviceName - Optional human-readable device label.
400
+ * @param params.deviceFingerprint - Optional device fingerprint.
401
+ */
402
+ approveCommonsSignIn(params: {
403
+ authorizeCode: string;
404
+ deviceName?: string;
405
+ deviceFingerprint?: string;
406
+ }): Promise<CommonsSignInActionResult>;
407
+ /**
408
+ * MECHANISM B (approver / Commons) — deny a sign-in request, cancelling the
409
+ * device-flow session so the RP stops waiting.
410
+ *
411
+ * @param authorizeCode - The public code being denied.
412
+ */
413
+ denyCommonsSignIn(authorizeCode: string): Promise<CommonsSignInActionResult>;
278
414
  /**
279
415
  * Refresh every device-local refresh-cookie slot in a single round trip
280
416
  * (Google-style multi-account rebuild).
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Identity Methods Mixin (self-sovereign identity layer)
3
+ *
4
+ * Provides typed access to Oxy's AtProto/Bluesky-flavoured identity &
5
+ * portability layer:
6
+ * - DID resolution (`did:web:oxy.so:u:<userId>`, derived on demand by the API).
7
+ * - The auth-method ↔ DID verification-method mapping and its reversibility
8
+ * (link/unlink an identity key, link a password) via the existing
9
+ * `/auth/link` surface.
10
+ * - Signed records: clients sign an envelope with their own cryptographic key
11
+ * (`SignatureService.signRecord` + the shared `canonicalize`) and publish it;
12
+ * anyone can fetch and verify it.
13
+ * - The signed data-export ("credible exit") bundle.
14
+ * - Verified-domain badges (prove ownership of `nate.com`).
15
+ *
16
+ * Wire shapes come from `@oxyhq/contracts` (`DidDocument`,
17
+ * `SignedRecordEnvelope`, `AuthMethodsResponse`, `VerifiedDomain`,
18
+ * `DomainVerificationInstructions`, `ExportBundle`) — the single source of truth
19
+ * the API validates its output against, so producer and consumer cannot drift.
20
+ *
21
+ * Identity signing is NATIVE-ONLY: the private key lives in native secure
22
+ * storage, so `linkIdentityKey`, `signRecord`, and `publishRecord` require an
23
+ * on-device identity and throw on web (where `KeyManager.getPublicKey()` is
24
+ * always `null`).
25
+ */
26
+ import type { AuthMethodsResponse, DidDocument, DomainVerificationInstructions, ExportBundle, SignedRecordEnvelope, VerifiedDomain } from '@oxyhq/contracts';
27
+ import type { OxyServicesBase } from '../OxyServices.base';
28
+ /** Record categories a client may sign and publish. */
29
+ export type IdentityRecordType = SignedRecordEnvelope['type'];
30
+ /** Auth-method types that can be unlinked via {@link OxyServicesIdentityMixin}. */
31
+ export type UnlinkableAuthMethodType = 'identity' | 'password' | 'google' | 'apple' | 'github';
32
+ /**
33
+ * Result of a link/unlink auth-method mutation (`POST /auth/link`,
34
+ * `DELETE /auth/link/:type`).
35
+ */
36
+ export interface LinkAuthMethodResult {
37
+ success: boolean;
38
+ message: string;
39
+ }
40
+ /**
41
+ * Result of publishing a signed record (`POST /identity/records`). Echoes the
42
+ * stored envelope plus the server's verification verdict.
43
+ */
44
+ export interface PublishRecordResult {
45
+ envelope: SignedRecordEnvelope;
46
+ verified: boolean;
47
+ }
48
+ /**
49
+ * Result of verifying a stored record (`GET /identity/records/:userId/:type/verify`).
50
+ * `verified` is the server's verdict; `reason` is present when it is `false`.
51
+ */
52
+ export interface VerifyRecordResult {
53
+ verified: boolean;
54
+ reason?: string;
55
+ }
56
+ /** Result of a successful domain verification (`POST /identity/domains/:domain/verify`). */
57
+ export interface VerifyDomainResult {
58
+ verified: boolean;
59
+ domain: VerifiedDomain;
60
+ }
61
+ /** Result of removing a verified domain (`DELETE /identity/domains/:domain`). */
62
+ export interface RemoveDomainResult {
63
+ success: boolean;
64
+ }
65
+ /**
66
+ * Derive a user's Oxy DID from their stable account id.
67
+ * `did:web:oxy.so:u:<userId>`.
68
+ */
69
+ export declare function buildUserDid(userId: string): string;
70
+ export declare function OxyServicesIdentityMixin<T extends typeof OxyServicesBase>(Base: T): {
71
+ new (...args: any[]): {
72
+ /**
73
+ * Resolve the W3C DID document for any user. The API derives it on demand
74
+ * from the account's `authMethods` + `publicKey` — there is no stored
75
+ * document. Public (no auth required); short-TTL cached.
76
+ *
77
+ * @param userId - The account's Mongo `_id`. URL-encoded into the path.
78
+ */
79
+ resolveDid(userId: string): Promise<DidDocument>;
80
+ /**
81
+ * The current user's DID (`did:web:oxy.so:u:<userId>`), derived locally from
82
+ * the access token's user id. Throws if no user is authenticated.
83
+ */
84
+ getMyDid(): string;
85
+ /** Resolve the current user's DID document. Requires an authenticated session. */
86
+ getMyDidDocument(): Promise<DidDocument>;
87
+ /**
88
+ * List the current user's linked authentication methods plus their DID.
89
+ * Each `identity` method carries a `verificationMethodId` linking it to its
90
+ * DID verification-method fragment.
91
+ */
92
+ listAuthMethods(): Promise<AuthMethodsResponse>;
93
+ /**
94
+ * Link the on-device cryptographic identity to the current account,
95
+ * upgrading it from custodial to self-sovereign. Signs a proof of private
96
+ * key ownership and posts it to `POST /auth/link`.
97
+ *
98
+ * NATIVE-ONLY: requires a stored identity (throws if `KeyManager` has no key
99
+ * or no user is authenticated). The signed payload is
100
+ * `JSON.stringify({ action: 'link_identity', userId, timestamp })` — the
101
+ * exact bytes the server reconstructs and verifies.
102
+ */
103
+ linkIdentityKey(): Promise<LinkAuthMethodResult>;
104
+ /**
105
+ * Link password authentication to the current account. Adds a `password`
106
+ * auth method (does not remove existing methods).
107
+ *
108
+ * @param email - The email to associate with password auth.
109
+ * @param password - The new password (server enforces strength rules).
110
+ */
111
+ linkPassword(email: string, password: string): Promise<LinkAuthMethodResult>;
112
+ /**
113
+ * Unlink an authentication method from the current account. The server
114
+ * refuses to remove the last remaining method (the account would become
115
+ * inaccessible). Unlinking `identity` downgrades the account to custodial.
116
+ *
117
+ * @param type - The auth-method type to remove.
118
+ */
119
+ unlinkAuthMethod(type: UnlinkableAuthMethodType): Promise<LinkAuthMethodResult>;
120
+ /**
121
+ * Sign a record with the on-device identity key, WITHOUT publishing it.
122
+ * The subject is the current user's DID. NATIVE-ONLY (requires a stored
123
+ * key). Use {@link publishRecord} to sign and store in one step.
124
+ *
125
+ * @param type - The record category.
126
+ * @param record - The arbitrary record payload to attest to.
127
+ */
128
+ signRecord(type: IdentityRecordType, record: Record<string, unknown>): Promise<SignedRecordEnvelope>;
129
+ /**
130
+ * Sign a record and publish it to the append-only record store
131
+ * (`POST /identity/records`). NATIVE-ONLY (requires a stored key).
132
+ *
133
+ * @param type - The record category.
134
+ * @param record - The arbitrary record payload to attest to.
135
+ */
136
+ publishRecord(type: IdentityRecordType, record: Record<string, unknown>): Promise<PublishRecordResult>;
137
+ /**
138
+ * Fetch a user's most recent signed record of a given type. Public (no auth
139
+ * required); short-TTL cached.
140
+ *
141
+ * @param userId - The subject account's Mongo `_id`.
142
+ * @param type - The record category to fetch.
143
+ */
144
+ getRecord(userId: string, type: IdentityRecordType): Promise<SignedRecordEnvelope>;
145
+ /**
146
+ * Ask the server to verify a user's stored record: it recomputes the
147
+ * canonical signing input, checks the signature, and asserts the signing key
148
+ * is a current verification method on the subject's DID.
149
+ *
150
+ * @param userId - The subject account's Mongo `_id`.
151
+ * @param type - The record category to verify.
152
+ */
153
+ verifyRecord(userId: string, type: IdentityRecordType): Promise<VerifyRecordResult>;
154
+ /**
155
+ * Download the current user's signed, open-format data-export bundle
156
+ * (`GET /users/me/export`) — the "credible exit" snapshot. Always carries an
157
+ * Oxy provenance `attestation`; carries an optional client `proof` when the
158
+ * account holds its own key.
159
+ */
160
+ exportMyData(): Promise<ExportBundle>;
161
+ /**
162
+ * Start verifying ownership of a domain. Returns the instructions: publish
163
+ * EITHER the DNS-TXT record OR the `/.well-known/oxy-domain` file, then call
164
+ * {@link verifyDomain}.
165
+ *
166
+ * @param domain - The domain to claim (e.g. `nate.com`).
167
+ */
168
+ requestDomainVerification(domain: string): Promise<DomainVerificationInstructions>;
169
+ /**
170
+ * Complete domain verification: the server checks the DNS-TXT record or
171
+ * well-known file and, on success, attaches the domain to the account
172
+ * (surfaced in the DID's `alsoKnownAs` and the user's `verifiedDomains`).
173
+ *
174
+ * @param domain - The domain previously requested via
175
+ * {@link requestDomainVerification}.
176
+ */
177
+ verifyDomain(domain: string): Promise<VerifyDomainResult>;
178
+ /** List the current user's verified domains. */
179
+ listDomains(): Promise<VerifiedDomain[]>;
180
+ /**
181
+ * Remove a verified domain from the current account.
182
+ * @param domain - The verified domain to remove.
183
+ */
184
+ removeDomain(domain: string): Promise<RemoveDomainResult>;
185
+ /**
186
+ * Bust the cached reads that an identity mutation invalidates: the current
187
+ * user (`/users/me*`), the linked auth-methods list, the verified-domains
188
+ * list, and the user's derived DID document (which embeds auth methods +
189
+ * verified domains, so it goes stale on link/unlink/domain changes).
190
+ *
191
+ * Internal helper (leading underscore); not part of the supported public
192
+ * surface. Public rather than `private` because mixins compose into an
193
+ * exported anonymous class, where TypeScript cannot represent a private
194
+ * member in the emitted declaration file (TS4094).
195
+ */
196
+ _invalidateIdentityCaches(userId: string | null): void;
197
+ httpService: import("../HttpService").HttpService;
198
+ cloudURL: string;
199
+ config: import("../OxyServices.base").OxyConfig;
200
+ __resetTokensForTests(): void;
201
+ makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
202
+ getBaseURL(): string;
203
+ getSessionBaseUrl(): string;
204
+ getClient(): import("../HttpService").HttpService;
205
+ createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
206
+ getMetrics(): {
207
+ totalRequests: number;
208
+ successfulRequests: number;
209
+ failedRequests: number;
210
+ cacheHits: number;
211
+ cacheMisses: number;
212
+ averageResponseTime: number;
213
+ };
214
+ clearCache(): void;
215
+ clearCacheEntry(key: string): void;
216
+ clearCacheByPrefix(prefix: string): number;
217
+ getCacheStats(): {
218
+ size: number;
219
+ hits: number;
220
+ misses: number;
221
+ hitRate: number;
222
+ };
223
+ getCloudURL(): string;
224
+ setTokens(accessToken: string): void;
225
+ clearTokens(): void;
226
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
227
+ _cachedUserId: string | null | undefined;
228
+ _cachedAccessToken: string | null;
229
+ getCurrentUserId(): string | null;
230
+ hasValidToken(): boolean;
231
+ getAccessToken(): string | null;
232
+ setActingAs(userId: string | null): void;
233
+ getActingAs(): string | null;
234
+ waitForAuth(timeoutMs?: number): Promise<boolean>;
235
+ withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
236
+ maxRetries?: number;
237
+ retryDelay?: number;
238
+ authTimeoutMs?: number;
239
+ }): Promise<T_1>;
240
+ validate(): Promise<boolean>;
241
+ handleError(error: unknown): Error;
242
+ healthCheck(): Promise<{
243
+ status: string;
244
+ users?: number;
245
+ timestamp?: string;
246
+ [key: string]: any;
247
+ }>;
248
+ };
249
+ } & T;
@@ -53,9 +53,12 @@ export declare function OxyServicesSsoMixin<T extends typeof OxyServicesBase>(Ba
53
53
  * @param code - The opaque single-use code delivered in the SSO return
54
54
  * fragment (see {@link parseSsoReturnFragment}). The central store burns
55
55
  * it atomically on exchange.
56
+ * @param state - The state value returned alongside the code. In browsers,
57
+ * when an SSO bounce state is still stored for the current origin, this
58
+ * must match before any token-committing exchange is attempted.
56
59
  * @returns The resolved {@link SessionLoginResponse}.
57
60
  */
58
- exchangeSsoCode(code: string): Promise<SessionLoginResponse>;
61
+ exchangeSsoCode(code: string, state?: string): Promise<SessionLoginResponse>;
59
62
  httpService: import("../HttpService").HttpService;
60
63
  cloudURL: string;
61
64
  config: import("../OxyServices.base").OxyConfig;