@oxyhq/core 3.4.13 → 3.4.15

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.
@@ -34,7 +34,9 @@ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServic
34
34
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount, } from './mixins/OxyServices.managedAccounts';
35
35
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
36
36
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
37
- export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull } from './utils/userIdentity';
37
+ export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity';
38
+ export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle';
39
+ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
38
40
  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';
39
41
  export type { Workspace, WorkspaceMember, WorkspaceRole, WorkspaceType, WorkspaceStatus, WorkspaceMemberStatus, CreateWorkspaceInput, UpdateWorkspaceInput, InviteWorkspaceMemberInput, UpdateWorkspaceMemberInput, TransferWorkspaceOwnershipInput, WorkspaceSuccessResult, } from './mixins/OxyServices.workspaces';
40
42
  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';
@@ -0,0 +1,26 @@
1
+ export interface UserHandleInput {
2
+ username?: string | null;
3
+ handle?: string | null;
4
+ instance?: string | null;
5
+ isFederated?: boolean | null;
6
+ type?: string | null;
7
+ federation?: {
8
+ domain?: string | null;
9
+ } | null;
10
+ }
11
+ export type CanonicalUserHandleInput = UserHandleInput;
12
+ /**
13
+ * Returns the normalized profile handle used by Oxy consumers for display and
14
+ * profile routing.
15
+ *
16
+ * Local users resolve to `username`. Federated users resolve to
17
+ * `username@instance` when the username does not already include an instance.
18
+ * Route-like values are rejected so callers do not accidentally turn paths or
19
+ * query strings into profile destinations.
20
+ */
21
+ export declare function getNormalizedUserHandle(user: UserHandleInput | null | undefined): string | null;
22
+ /**
23
+ * Compatibility alias for the first public name shipped with this helper.
24
+ * Prefer {@link getNormalizedUserHandle} in new code.
25
+ */
26
+ export declare function getCanonicalUserHandle(user: CanonicalUserHandleInput | null | undefined): string | null;
@@ -2,7 +2,16 @@ interface UserIdentityInput {
2
2
  id?: unknown;
3
3
  _id?: unknown;
4
4
  }
5
+ /**
6
+ * Returns the stable SDK user id from API payloads that may use either `id` or
7
+ * Mongo-style `_id`.
8
+ */
5
9
  export declare function getNormalizedUserId(user: UserIdentityInput | null | undefined): string | null;
10
+ /**
11
+ * Normalizes a user payload to always expose `id`. Throws when the payload does
12
+ * not contain a usable id, because SDK callers should never receive anonymous
13
+ * user objects from authenticated identity endpoints.
14
+ */
6
15
  export declare function normalizeUserIdentity<T extends UserIdentityInput>(user: T): T & {
7
16
  id: string;
8
17
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.4.13",
3
+ "version": "3.4.15",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,5 +1,6 @@
1
1
  import { OxyServices } from '../OxyServices';
2
2
  import { getNormalizedUserId, normalizeUserIdentity } from '../utils/userIdentity';
3
+ import { getCanonicalUserHandle, getNormalizedUserHandle } from '../utils/userHandle';
3
4
 
4
5
  interface FetchCall {
5
6
  url: string;
@@ -71,3 +72,72 @@ describe('user identity normalization', () => {
71
72
  expect(validation.user.username).toBe('nate');
72
73
  });
73
74
  });
75
+
76
+ describe('getNormalizedUserHandle', () => {
77
+ it('normalizes local usernames without route prefixes', () => {
78
+ expect(getNormalizedUserHandle({ username: ' nate ' })).toBe('nate');
79
+ expect(getNormalizedUserHandle({ username: '@nate' })).toBe('nate');
80
+ });
81
+
82
+ it('falls back to handle when username is missing', () => {
83
+ expect(getNormalizedUserHandle({ handle: '@nate' })).toBe('nate');
84
+ });
85
+
86
+ it('builds federated handles from username and instance', () => {
87
+ expect(getNormalizedUserHandle({
88
+ username: 'joannastern',
89
+ isFederated: true,
90
+ instance: 'threads.net',
91
+ })).toBe('joannastern@threads.net');
92
+ });
93
+
94
+ it('does not append an instance twice', () => {
95
+ expect(getNormalizedUserHandle({
96
+ username: '@joannastern@threads.net',
97
+ type: 'federated',
98
+ instance: 'threads.net',
99
+ })).toBe('joannastern@threads.net');
100
+ });
101
+
102
+ it('uses federation domain as the federated instance source', () => {
103
+ expect(getNormalizedUserHandle({
104
+ username: 'alice',
105
+ isFederated: true,
106
+ federation: { domain: '@example.social' },
107
+ })).toBe('alice@example.social');
108
+ });
109
+
110
+ it('does not use instance for local users', () => {
111
+ expect(getNormalizedUserHandle({
112
+ username: 'alice',
113
+ instance: 'example.social',
114
+ })).toBe('alice');
115
+ });
116
+
117
+ it('keeps case while normalizing whitespace', () => {
118
+ expect(getNormalizedUserHandle({
119
+ username: ' Alice ',
120
+ isFederated: true,
121
+ instance: ' Example.Social ',
122
+ })).toBe('Alice@Example.Social');
123
+ });
124
+
125
+ it('does not turn an invalid instance into a federated suffix', () => {
126
+ expect(getNormalizedUserHandle({
127
+ username: 'alice',
128
+ isFederated: true,
129
+ instance: 'example.social/users/alice',
130
+ })).toBe('alice');
131
+ });
132
+
133
+ it('rejects empty and route-like values', () => {
134
+ expect(getNormalizedUserHandle({ username: '' })).toBeNull();
135
+ expect(getNormalizedUserHandle({ username: 'alice/about' })).toBeNull();
136
+ expect(getNormalizedUserHandle({ username: 'alice?tab=posts' })).toBeNull();
137
+ expect(getNormalizedUserHandle({ username: 'alice#posts' })).toBeNull();
138
+ });
139
+
140
+ it('keeps the compatibility alias wired to the normalized implementation', () => {
141
+ expect(getCanonicalUserHandle({ username: '@nate' })).toBe('nate');
142
+ });
143
+ });
package/src/index.ts CHANGED
@@ -62,7 +62,20 @@ export type {
62
62
  ContactDiscoveryResponse,
63
63
  } from './mixins/OxyServices.contacts';
64
64
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
65
- export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull } from './utils/userIdentity';
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // User identity and handles
68
+ // ---------------------------------------------------------------------------
69
+ export {
70
+ getNormalizedUserId,
71
+ normalizeUserIdentity,
72
+ normalizeUserIdentityOrNull,
73
+ } from './utils/userIdentity';
74
+ export {
75
+ getCanonicalUserHandle,
76
+ getNormalizedUserHandle,
77
+ } from './utils/userHandle';
78
+ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
66
79
 
67
80
  // ---------------------------------------------------------------------------
68
81
  // Applications (multi-user apps: membership, roles, credentials)
@@ -2,7 +2,6 @@
2
2
  * Async utilities for common asynchronous patterns and error handling
3
3
  */
4
4
 
5
- import { TTLCache, registerCacheForCleanup } from './cache';
6
5
  import { logger } from './loggerUtils';
7
6
 
8
7
  /**
@@ -278,4 +277,4 @@ export async function retryOnError<T>(
278
277
  const errorCode = error?.code || error?.status || error?.message;
279
278
  return retryableErrors.includes(errorCode);
280
279
  });
281
- }
280
+ }
@@ -0,0 +1,49 @@
1
+ export interface UserHandleInput {
2
+ username?: string | null;
3
+ handle?: string | null;
4
+ instance?: string | null;
5
+ isFederated?: boolean | null;
6
+ type?: string | null;
7
+ federation?: {
8
+ domain?: string | null;
9
+ } | null;
10
+ }
11
+
12
+ export type CanonicalUserHandleInput = UserHandleInput;
13
+
14
+ function normalizeHandlePart(value?: string | null): string | null {
15
+ const trimmed = value?.trim().replace(/^@+/, '');
16
+ if (!trimmed || /[/?#]/.test(trimmed)) return null;
17
+ return trimmed;
18
+ }
19
+
20
+ /**
21
+ * Returns the normalized profile handle used by Oxy consumers for display and
22
+ * profile routing.
23
+ *
24
+ * Local users resolve to `username`. Federated users resolve to
25
+ * `username@instance` when the username does not already include an instance.
26
+ * Route-like values are rejected so callers do not accidentally turn paths or
27
+ * query strings into profile destinations.
28
+ */
29
+ export function getNormalizedUserHandle(user: UserHandleInput | null | undefined): string | null {
30
+ const username = normalizeHandlePart(user?.username ?? user?.handle);
31
+ if (!username) return null;
32
+
33
+ const isFederated = user?.isFederated === true || user?.type === 'federated';
34
+ const instance = normalizeHandlePart(user?.instance ?? user?.federation?.domain);
35
+
36
+ if (isFederated && instance && !username.includes('@')) {
37
+ return `${username}@${instance}`;
38
+ }
39
+
40
+ return username;
41
+ }
42
+
43
+ /**
44
+ * Compatibility alias for the first public name shipped with this helper.
45
+ * Prefer {@link getNormalizedUserHandle} in new code.
46
+ */
47
+ export function getCanonicalUserHandle(user: CanonicalUserHandleInput | null | undefined): string | null {
48
+ return getNormalizedUserHandle(user);
49
+ }
@@ -27,6 +27,10 @@ function stringifyIdentity(value: unknown): string | null {
27
27
  return null;
28
28
  }
29
29
 
30
+ /**
31
+ * Returns the stable SDK user id from API payloads that may use either `id` or
32
+ * Mongo-style `_id`.
33
+ */
30
34
  export function getNormalizedUserId(user: UserIdentityInput | null | undefined): string | null {
31
35
  if (!user) {
32
36
  return null;
@@ -35,6 +39,11 @@ export function getNormalizedUserId(user: UserIdentityInput | null | undefined):
35
39
  return stringifyIdentity(user.id) ?? stringifyIdentity(user._id);
36
40
  }
37
41
 
42
+ /**
43
+ * Normalizes a user payload to always expose `id`. Throws when the payload does
44
+ * not contain a usable id, because SDK callers should never receive anonymous
45
+ * user objects from authenticated identity endpoints.
46
+ */
38
47
  export function normalizeUserIdentity<T extends UserIdentityInput>(user: T): T & { id: string } {
39
48
  const id = getNormalizedUserId(user);
40
49
  if (!id) {