@oxyhq/core 3.11.0 → 3.12.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/crypto/signatureService.js +88 -2
- package/dist/cjs/index.js +19 -4
- package/dist/cjs/mixins/OxyServices.civic.js +611 -0
- package/dist/cjs/mixins/index.js +3 -0
- package/dist/cjs/utils/profileLinks.js +52 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/signatureService.js +87 -2
- package/dist/esm/index.js +11 -1
- package/dist/esm/mixins/OxyServices.civic.js +605 -0
- package/dist/esm/mixins/index.js +3 -0
- package/dist/esm/utils/profileLinks.js +49 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/signatureService.d.ts +54 -3
- package/dist/types/index.d.ts +5 -1
- package/dist/types/mixins/OxyServices.civic.d.ts +512 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/utils/profileLinks.d.ts +36 -0
- package/package.json +2 -2
- package/src/crypto/__tests__/signedRecord.test.ts +221 -1
- package/src/crypto/signatureService.ts +102 -3
- package/src/index.ts +29 -1
- package/src/mixins/OxyServices.civic.ts +956 -0
- package/src/mixins/__tests__/OxyServices.civic.test.ts +1097 -0
- package/src/mixins/index.ts +4 -0
- package/src/utils/__tests__/profileLinks.test.ts +126 -0
- package/src/utils/profileLinks.ts +74 -0
package/src/mixins/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { OxyServicesTopicsMixin } from './OxyServices.topics';
|
|
|
30
30
|
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts';
|
|
31
31
|
import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
32
32
|
import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
33
|
+
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Instance shape of every mixin in the pipeline, intersected. The runtime
|
|
@@ -64,6 +65,7 @@ type AllMixinInstances =
|
|
|
64
65
|
& InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>>
|
|
65
66
|
& InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>>
|
|
66
67
|
& InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>>
|
|
68
|
+
& InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>>
|
|
67
69
|
& InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -130,6 +132,8 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
130
132
|
OxyServicesManagedAccountsMixin,
|
|
131
133
|
OxyServicesContactsMixin,
|
|
132
134
|
OxyServicesAppDataMixin,
|
|
135
|
+
// Civic / Commons "Oxy ID" (public signed cards, Oxy ID QR payload)
|
|
136
|
+
OxyServicesCivicMixin,
|
|
133
137
|
|
|
134
138
|
// Utility (last, can use all above)
|
|
135
139
|
OxyServicesUtilityMixin,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { normalizeProfileLinks, type ProfileLink } from '../profileLinks';
|
|
2
|
+
|
|
3
|
+
describe('normalizeProfileLinks', () => {
|
|
4
|
+
describe('linksMetadata path (preferred)', () => {
|
|
5
|
+
it('maps title + url and uses the entry id when present', () => {
|
|
6
|
+
const result = normalizeProfileLinks([
|
|
7
|
+
{ url: 'https://oxy.so', title: 'Oxy', id: 'abc' },
|
|
8
|
+
]);
|
|
9
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
10
|
+
{ id: 'abc', title: 'Oxy', url: 'https://oxy.so' },
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('falls back to the index when an entry has no id', () => {
|
|
15
|
+
const result = normalizeProfileLinks([
|
|
16
|
+
{ url: 'https://a.example', title: 'A' },
|
|
17
|
+
{ url: 'https://b.example', title: 'B' },
|
|
18
|
+
]);
|
|
19
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
20
|
+
{ id: '0', title: 'A', url: 'https://a.example' },
|
|
21
|
+
{ id: '1', title: 'B', url: 'https://b.example' },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('omits title when absent', () => {
|
|
26
|
+
const result = normalizeProfileLinks([{ url: 'https://no-title.example' }]);
|
|
27
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
28
|
+
{ id: '0', url: 'https://no-title.example' },
|
|
29
|
+
]);
|
|
30
|
+
expect(result[0]).not.toHaveProperty('title');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('drops entries with missing or empty url and keeps stable index ids', () => {
|
|
34
|
+
const result = normalizeProfileLinks([
|
|
35
|
+
{ url: 'https://keep.example', title: 'Keep', id: 'keep' },
|
|
36
|
+
{ url: '', title: 'Empty' },
|
|
37
|
+
{ url: ' ', title: 'Whitespace' },
|
|
38
|
+
{ url: 'https://second.example', title: 'Second' },
|
|
39
|
+
]);
|
|
40
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
41
|
+
{ id: 'keep', title: 'Keep', url: 'https://keep.example' },
|
|
42
|
+
// index is preserved from the source array position (3), not re-numbered
|
|
43
|
+
{ id: '3', title: 'Second', url: 'https://second.example' },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('trims surrounding whitespace from urls', () => {
|
|
48
|
+
const result = normalizeProfileLinks([{ url: ' https://trim.example ' }]);
|
|
49
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
50
|
+
{ id: '0', url: 'https://trim.example' },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does NOT add a scheme to bare urls', () => {
|
|
55
|
+
const result = normalizeProfileLinks([{ url: 'oxy.so', title: 'Bare' }]);
|
|
56
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
57
|
+
{ id: '0', title: 'Bare', url: 'oxy.so' },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('legacy links path (no linksMetadata)', () => {
|
|
63
|
+
it('maps strings to { id, url } without titles', () => {
|
|
64
|
+
const result = normalizeProfileLinks(undefined, [
|
|
65
|
+
'https://a.example',
|
|
66
|
+
'https://b.example',
|
|
67
|
+
]);
|
|
68
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
69
|
+
{ id: '0', url: 'https://a.example' },
|
|
70
|
+
{ id: '1', url: 'https://b.example' },
|
|
71
|
+
]);
|
|
72
|
+
expect(result.every((link) => !('title' in link))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('drops empty and whitespace-only strings, preserving source index ids', () => {
|
|
76
|
+
const result = normalizeProfileLinks(undefined, [
|
|
77
|
+
'https://keep.example',
|
|
78
|
+
'',
|
|
79
|
+
' ',
|
|
80
|
+
'https://second.example',
|
|
81
|
+
]);
|
|
82
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
83
|
+
{ id: '0', url: 'https://keep.example' },
|
|
84
|
+
{ id: '3', url: 'https://second.example' },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('fall-through and empty inputs', () => {
|
|
90
|
+
it('returns [] when both inputs are absent', () => {
|
|
91
|
+
expect(normalizeProfileLinks()).toEqual([]);
|
|
92
|
+
expect(normalizeProfileLinks(undefined, undefined)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns [] when both inputs are empty arrays', () => {
|
|
96
|
+
expect(normalizeProfileLinks([], [])).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('falls through to legacy links when linksMetadata is empty', () => {
|
|
100
|
+
const result = normalizeProfileLinks([], ['https://legacy.example']);
|
|
101
|
+
expect(result).toEqual<ProfileLink[]>([
|
|
102
|
+
{ id: '0', url: 'https://legacy.example' },
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns [] (does NOT fall back to links) when linksMetadata is non-empty but every entry is dropped', () => {
|
|
107
|
+
const result = normalizeProfileLinks(
|
|
108
|
+
[{ url: '' }, { url: ' ' }],
|
|
109
|
+
['https://legacy.example'],
|
|
110
|
+
);
|
|
111
|
+
expect(result).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('is pure — does not mutate its inputs', () => {
|
|
116
|
+
const linksMetadata = [{ url: 'https://oxy.so', title: 'Oxy', id: 'abc' }];
|
|
117
|
+
const links = ['https://legacy.example'];
|
|
118
|
+
const metadataSnapshot = JSON.stringify(linksMetadata);
|
|
119
|
+
const linksSnapshot = JSON.stringify(links);
|
|
120
|
+
|
|
121
|
+
normalizeProfileLinks(linksMetadata, links);
|
|
122
|
+
|
|
123
|
+
expect(JSON.stringify(linksMetadata)).toBe(metadataSnapshot);
|
|
124
|
+
expect(JSON.stringify(links)).toBe(linksSnapshot);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized profile link shape for display.
|
|
3
|
+
*
|
|
4
|
+
* `id` is a stable key for list rendering (the source entry's id when present,
|
|
5
|
+
* otherwise the source index as a string). `url` is always a non-empty string.
|
|
6
|
+
*/
|
|
7
|
+
export interface ProfileLink {
|
|
8
|
+
id: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Source shape of a single `User.linksMetadata` entry. */
|
|
14
|
+
export interface ProfileLinkMetadata {
|
|
15
|
+
url: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
image?: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanUrl(value: unknown): string | null {
|
|
23
|
+
if (typeof value !== 'string') return null;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes a user's profile links into a clean display shape.
|
|
30
|
+
*
|
|
31
|
+
* Pure, no side effects, no I/O.
|
|
32
|
+
*
|
|
33
|
+
* - Prefers `linksMetadata` when it is a non-empty array: maps each entry to
|
|
34
|
+
* `{ id, title, url }`, using `entry.id` when present and falling back to the
|
|
35
|
+
* entry index. Entries without a non-empty string `url` are dropped.
|
|
36
|
+
* - Otherwise falls back to the legacy `links` string array: maps each string to
|
|
37
|
+
* `{ id: <index>, url }` (no title). Empty/non-string values are dropped.
|
|
38
|
+
* - Returns `[]` when both are absent or empty (including when `linksMetadata`
|
|
39
|
+
* is present but every entry is dropped — it does NOT fall back to `links`).
|
|
40
|
+
*
|
|
41
|
+
* URLs are trimmed and blanks are filtered out. This does NOT add a scheme such
|
|
42
|
+
* as `https://`; prefixing is a display concern left to the caller.
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeProfileLinks(
|
|
45
|
+
linksMetadata?: ProfileLinkMetadata[],
|
|
46
|
+
links?: string[],
|
|
47
|
+
): ProfileLink[] {
|
|
48
|
+
if (Array.isArray(linksMetadata) && linksMetadata.length > 0) {
|
|
49
|
+
const result: ProfileLink[] = [];
|
|
50
|
+
linksMetadata.forEach((entry, index) => {
|
|
51
|
+
const url = cleanUrl(entry?.url);
|
|
52
|
+
if (!url) return;
|
|
53
|
+
const id =
|
|
54
|
+
typeof entry?.id === 'string' && entry.id.trim().length > 0
|
|
55
|
+
? entry.id
|
|
56
|
+
: String(index);
|
|
57
|
+
const title = typeof entry?.title === 'string' ? entry.title : undefined;
|
|
58
|
+
result.push({ id, url, ...(title !== undefined ? { title } : {}) });
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(links) && links.length > 0) {
|
|
64
|
+
const result: ProfileLink[] = [];
|
|
65
|
+
links.forEach((value, index) => {
|
|
66
|
+
const url = cleanUrl(value);
|
|
67
|
+
if (!url) return;
|
|
68
|
+
result.push({ id: String(index), url });
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [];
|
|
74
|
+
}
|