@oxy.so/core 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/OxyServices.errors.js +28 -1
  3. package/dist/cjs/crypto/keyManager.js +5 -3
  4. package/dist/cjs/i18n/locales/en-US.json +18 -5
  5. package/dist/cjs/i18n/locales/es-ES.json +18 -5
  6. package/dist/cjs/i18n/locales/locales/en-US.json +18 -5
  7. package/dist/cjs/i18n/locales/locales/es-ES.json +18 -5
  8. package/dist/cjs/mixins/OxyServices.agency.js +113 -0
  9. package/dist/cjs/mixins/OxyServices.assets.js +90 -0
  10. package/dist/cjs/mixins/OxyServices.connectedApps.js +20 -3
  11. package/dist/cjs/mixins/OxyServices.utility.js +1 -21
  12. package/dist/cjs/mixins/index.js +3 -4
  13. package/dist/cjs/package.json +3 -0
  14. package/dist/cjs/server/index.js +6 -1
  15. package/dist/cjs/server/traffic.js +107 -0
  16. package/dist/cjs/server/trafficNodeHttp.js +86 -0
  17. package/dist/cjs/utils/textNormalization.js +1 -1
  18. package/dist/esm/.tsbuildinfo +1 -1
  19. package/dist/esm/OxyServices.errors.js +26 -0
  20. package/dist/esm/crypto/keyManager.js +5 -3
  21. package/dist/esm/i18n/locales/en-US.json +18 -5
  22. package/dist/esm/i18n/locales/es-ES.json +18 -5
  23. package/dist/esm/i18n/locales/locales/en-US.json +18 -5
  24. package/dist/esm/i18n/locales/locales/es-ES.json +18 -5
  25. package/dist/esm/mixins/OxyServices.agency.js +110 -0
  26. package/dist/esm/mixins/OxyServices.assets.js +91 -1
  27. package/dist/esm/mixins/OxyServices.connectedApps.js +20 -3
  28. package/dist/esm/mixins/OxyServices.utility.js +1 -21
  29. package/dist/esm/mixins/index.js +3 -4
  30. package/dist/esm/package.json +3 -0
  31. package/dist/esm/server/index.js +2 -0
  32. package/dist/esm/server/traffic.js +104 -0
  33. package/dist/esm/server/trafficNodeHttp.js +79 -0
  34. package/dist/esm/utils/textNormalization.js +1 -1
  35. package/dist/types/.tsbuildinfo +1 -1
  36. package/dist/types/OxyServices.errors.d.ts +24 -0
  37. package/dist/types/crypto/keyManager.d.ts +4 -2
  38. package/dist/types/index.d.ts +2 -1
  39. package/dist/types/mixins/OxyServices.agency.d.ts +135 -0
  40. package/dist/types/mixins/OxyServices.assets.d.ts +43 -1
  41. package/dist/types/mixins/OxyServices.connectedApps.d.ts +15 -0
  42. package/dist/types/mixins/OxyServices.links.d.ts +2 -2
  43. package/dist/types/mixins/OxyServices.utility.d.ts +0 -9
  44. package/dist/types/mixins/index.d.ts +2 -2
  45. package/dist/types/models/interfaces.d.ts +26 -0
  46. package/dist/types/server/index.d.ts +3 -0
  47. package/dist/types/server/traffic.d.ts +27 -0
  48. package/dist/types/server/trafficNodeHttp.d.ts +6 -0
  49. package/dist/types/utils/textNormalization.d.ts +1 -1
  50. package/package.json +7 -6
  51. package/src/OxyServices.errors.ts +36 -0
  52. package/src/crypto/keyManager.ts +5 -4
  53. package/src/i18n/locales/en-US.json +18 -5
  54. package/src/i18n/locales/es-ES.json +18 -5
  55. package/src/index.ts +15 -0
  56. package/src/mixins/OxyServices.agency.ts +277 -0
  57. package/src/mixins/OxyServices.assets.ts +105 -2
  58. package/src/mixins/OxyServices.connectedApps.ts +42 -4
  59. package/src/mixins/OxyServices.utility.ts +1 -26
  60. package/src/mixins/__tests__/OxyServices.serviceLinkedDownloadUrl.test.ts +139 -0
  61. package/src/mixins/__tests__/agency.test.ts +120 -0
  62. package/src/mixins/__tests__/connectedApps.test.ts +23 -0
  63. package/src/mixins/index.ts +4 -5
  64. package/src/models/interfaces.ts +27 -0
  65. package/src/server/__tests__/traffic.test.ts +49 -0
  66. package/src/server/__tests__/trafficNodeHttp.test.ts +38 -0
  67. package/src/server/index.ts +5 -0
  68. package/src/server/traffic.ts +116 -0
  69. package/src/server/trafficNodeHttp.ts +69 -0
  70. package/src/utils/textNormalization.ts +1 -1
  71. package/src/mixins/OxyServices.links.ts +0 -107
  72. package/src/mixins/__tests__/OxyServices.links.test.ts +0 -154
@@ -90,7 +90,7 @@ const LEADING_HORIZONTAL_WHITESPACE = /\n +/g;
90
90
  const EXCESS_BLANK_LINES = /\n{3,}/g;
91
91
 
92
92
  /**
93
- * Normalize a SINGLE-LINE text value: page/link-preview titles, `siteName`,
93
+ * Normalize a SINGLE-LINE text value such as a page title or `siteName`,
94
94
  * display names, image alt text, handles, profile field labels.
95
95
  *
96
96
  * A line break in such a value is never meaningful — it is an artifact of the
@@ -1,107 +0,0 @@
1
- /**
2
- * Link Preview (unfurl) Mixin
3
- *
4
- * Resolves link previews ("unfurls") through the Oxy API so every Oxy app
5
- * stops scraping link metadata locally. The API owns resolution and re-hosts
6
- * the preview `image`/`favicon` on Oxy media (`cloud.oxy.so/<fileId>`), so
7
- * consumers render the returned URLs directly with no per-app proxy.
8
- *
9
- * Wire shapes (`LinkPreview`, `LinkPreviewBatchResponse`) are the single source
10
- * of truth in `@oxy.so/contracts`; this mixin imports them rather than
11
- * redefining them so producer (oxy-api) and consumers cannot drift.
12
- *
13
- * Caching note: these GET/POST reads are NOT cached at the SDK layer (`cache:
14
- * false`). A preview can be returned `'pending'` first and `'resolved'` on a
15
- * later read, so an SDK GET cache would pin the stale `'pending'` snapshot.
16
- * App-side caching (React Query / stores) owns this responsibility.
17
- */
18
- import type { LinkPreview } from '@oxy.so/contracts';
19
- import type { OxyServicesBase } from '../OxyServices.base';
20
- import { buildUrl } from '../utils/apiUtils';
21
-
22
- /**
23
- * Maximum number of URLs sent per `POST /links/previews` request. Matches the
24
- * server-side batch cap (`linkPreviewBatchRequestSchema`'s `.max(50)`); larger
25
- * inputs are split into multiple chunked calls and the result maps merged,
26
- * mirroring how `getUsersByIds` chunks at 100.
27
- */
28
- const LINK_PREVIEWS_CHUNK_SIZE = 50;
29
-
30
- export function OxyServicesLinksMixin<T extends typeof OxyServicesBase>(Base: T) {
31
- return class extends Base {
32
- constructor(...args: any[]) {
33
- super(...(args as [any]));
34
- }
35
-
36
- /**
37
- * Resolve a single link preview via `GET /links/preview?url=<encoded>&wait=0|1`.
38
- *
39
- * @param url - The URL to unfurl. Sent percent-encoded in the query string.
40
- * @param opts.wait - When `true`, asks the server to resolve synchronously
41
- * (`wait=1`) instead of returning a `'pending'` placeholder for a
42
- * first-seen URL. Defaults to `false` (`wait=0`).
43
- *
44
- * Not cached at the SDK layer: a `'pending'` result can become `'resolved'`
45
- * on a later read, so caching here would serve the stale placeholder.
46
- */
47
- async getLinkPreview(url: string, opts?: { wait?: boolean }): Promise<LinkPreview> {
48
- try {
49
- const path = buildUrl('/links/preview', { url, wait: opts?.wait ? 1 : 0 });
50
- return await this.makeRequest<LinkPreview>('GET', path, undefined, { cache: false });
51
- } catch (error) {
52
- throw this.handleError(error);
53
- }
54
- }
55
-
56
- /**
57
- * Resolve multiple link previews via `POST /links/previews` (body `{ urls }`).
58
- *
59
- * Inputs are de-duplicated and split into chunks of {@link LINK_PREVIEWS_CHUNK_SIZE}
60
- * (the server-side cap). Chunks run concurrently and their result maps are
61
- * merged into a single result keyed by the REQUESTED url (the exact string
62
- * passed in `urls`) — matching the batch contract — so a caller can always
63
- * look its own input back up.
64
- *
65
- * `makeRequest` unwraps the API's top-level `{ data }` envelope (same as
66
- * `getUsersByIds`'s `makeServiceRequest<User[]>`), so each chunk response is
67
- * already the `Record<url, LinkPreview>` map — merge it directly.
68
- *
69
- * An empty / whitespace-only input resolves immediately with `{}` and
70
- * performs no network call. A failure in any chunk surfaces (via
71
- * `handleError`) rather than being swallowed.
72
- */
73
- async getLinkPreviews(urls: string[]): Promise<Record<string, LinkPreview>> {
74
- const uniqueUrls = Array.from(
75
- new Set(urls.filter((u): u is string => typeof u === 'string' && u.trim().length > 0)),
76
- );
77
- if (uniqueUrls.length === 0) {
78
- return {};
79
- }
80
-
81
- const chunks: string[][] = [];
82
- for (let i = 0; i < uniqueUrls.length; i += LINK_PREVIEWS_CHUNK_SIZE) {
83
- chunks.push(uniqueUrls.slice(i, i + LINK_PREVIEWS_CHUNK_SIZE));
84
- }
85
-
86
- try {
87
- const responses = await Promise.all(
88
- chunks.map((chunk) =>
89
- this.makeRequest<Record<string, LinkPreview>>(
90
- 'POST',
91
- '/links/previews',
92
- { urls: chunk },
93
- { cache: false },
94
- ),
95
- ),
96
- );
97
-
98
- return responses.reduce<Record<string, LinkPreview>>(
99
- (merged, response) => Object.assign(merged, response ?? {}),
100
- {},
101
- );
102
- } catch (error) {
103
- throw this.handleError(error);
104
- }
105
- }
106
- };
107
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * Link-preview mixin tests.
3
- *
4
- * Stubs `makeRequest` so the tests run with no network, then asserts:
5
- * - `getLinkPreview` builds `GET /links/preview?url=<percent-encoded>&wait=0|1`,
6
- * percent-encoding the target URL so its `?`/`&`/`=` cannot break out of the
7
- * query string, defaults `wait` to `0`, sends `wait=1` when `{ wait: true }`,
8
- * reads uncached (`cache:false`), returns the preview, and surfaces errors.
9
- * - `getLinkPreviews` de-duplicates input, no-ops to `{}` on empty input,
10
- * chunks at 50 URLs/request (`POST /links/previews`), merges every chunk's
11
- * `data` map keyed by the requested url, and surfaces a chunk failure.
12
- */
13
-
14
- import type { LinkPreview } from '@oxy.so/contracts';
15
- import { OxyServices } from '../../OxyServices';
16
-
17
- const sampleResolved: LinkPreview = {
18
- url: 'https://news.example.com/a',
19
- status: 'resolved',
20
- title: 'Headline',
21
- description: 'Lede',
22
- image: 'https://cloud.oxy.so/img123',
23
- siteName: 'Example News',
24
- favicon: 'https://cloud.oxy.so/fav123',
25
- resolvedAt: '2026-06-28T00:00:00.000Z',
26
- };
27
-
28
- describe('OxyServices.links', () => {
29
- let oxy: OxyServices;
30
- let makeRequestSpy: jest.SpyInstance;
31
-
32
- beforeEach(() => {
33
- oxy = new OxyServices({ baseURL: 'http://test.invalid' });
34
- makeRequestSpy = jest.spyOn(oxy, 'makeRequest');
35
- });
36
-
37
- afterEach(() => {
38
- jest.restoreAllMocks();
39
- });
40
-
41
- describe('getLinkPreview', () => {
42
- it('percent-encodes the URL and defaults wait=0', async () => {
43
- makeRequestSpy.mockResolvedValueOnce(sampleResolved);
44
-
45
- const result = await oxy.getLinkPreview('https://news.example.com/a?b=c&d=e');
46
-
47
- expect(result).toEqual(sampleResolved);
48
- expect(makeRequestSpy).toHaveBeenCalledTimes(1);
49
- expect(makeRequestSpy).toHaveBeenCalledWith(
50
- 'GET',
51
- '/links/preview?url=https%3A%2F%2Fnews.example.com%2Fa%3Fb%3Dc%26d%3De&wait=0',
52
- undefined,
53
- { cache: false },
54
- );
55
- });
56
-
57
- it('sends wait=1 when opts.wait is true', async () => {
58
- makeRequestSpy.mockResolvedValueOnce({ url: 'https://x.test/', status: 'pending' });
59
-
60
- await oxy.getLinkPreview('https://x.test/', { wait: true });
61
-
62
- expect(makeRequestSpy).toHaveBeenCalledWith(
63
- 'GET',
64
- '/links/preview?url=https%3A%2F%2Fx.test%2F&wait=1',
65
- undefined,
66
- { cache: false },
67
- );
68
- });
69
-
70
- it('surfaces errors via handleError', async () => {
71
- makeRequestSpy.mockRejectedValueOnce(new Error('boom'));
72
-
73
- await expect(oxy.getLinkPreview('https://x.test/')).rejects.toThrow('boom');
74
- });
75
- });
76
-
77
- describe('getLinkPreviews', () => {
78
- it('returns {} and performs no network call for empty / whitespace input', async () => {
79
- await expect(oxy.getLinkPreviews([])).resolves.toEqual({});
80
- await expect(oxy.getLinkPreviews(['', ' '])).resolves.toEqual({});
81
- expect(makeRequestSpy).not.toHaveBeenCalled();
82
- });
83
-
84
- it('de-duplicates and sends a single chunk for <= 50 unique URLs', async () => {
85
- // makeRequest unwraps the API's `{ data }` envelope, so the resolved value
86
- // is the bare record (NOT `{ data: {...} }`) — mirror that real shape here.
87
- const record: Record<string, LinkPreview> = { 'https://a.test/': sampleResolved };
88
- makeRequestSpy.mockResolvedValueOnce(record);
89
-
90
- const result = await oxy.getLinkPreviews([
91
- 'https://a.test/',
92
- 'https://a.test/', // duplicate
93
- ' ', // dropped
94
- ]);
95
-
96
- expect(result).toEqual(record);
97
- expect(makeRequestSpy).toHaveBeenCalledTimes(1);
98
- expect(makeRequestSpy).toHaveBeenCalledWith(
99
- 'POST',
100
- '/links/previews',
101
- { urls: ['https://a.test/'] },
102
- { cache: false },
103
- );
104
- });
105
-
106
- it('chunks at 50 URLs per request and merges each chunk data map', async () => {
107
- const urls = Array.from({ length: 120 }, (_, i) => `https://site.test/${i}`);
108
-
109
- makeRequestSpy.mockImplementation(
110
- async (
111
- _method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
112
- _url: string,
113
- data?: { urls: string[] },
114
- ): Promise<Record<string, LinkPreview>> => {
115
- const chunkUrls = data?.urls ?? [];
116
- const dataMap: Record<string, LinkPreview> = {};
117
- for (const u of chunkUrls) {
118
- dataMap[u] = { url: u, status: 'resolved', title: `t-${u}` };
119
- }
120
- return dataMap;
121
- },
122
- );
123
-
124
- const result = await oxy.getLinkPreviews(urls);
125
-
126
- // 120 unique URLs => 50 + 50 + 20 across three POSTs.
127
- expect(makeRequestSpy).toHaveBeenCalledTimes(3);
128
- const chunkSizes = makeRequestSpy.mock.calls.map((call) => (call[2] as { urls: string[] }).urls.length);
129
- expect(chunkSizes).toEqual([50, 50, 20]);
130
-
131
- // Every requested URL is present in the merged, request-keyed map.
132
- expect(Object.keys(result)).toHaveLength(120);
133
- expect(result['https://site.test/0']).toEqual({
134
- url: 'https://site.test/0',
135
- status: 'resolved',
136
- title: 't-https://site.test/0',
137
- });
138
- expect(result['https://site.test/119']).toEqual({
139
- url: 'https://site.test/119',
140
- status: 'resolved',
141
- title: 't-https://site.test/119',
142
- });
143
- });
144
-
145
- it('surfaces a chunk failure via handleError', async () => {
146
- const urls = Array.from({ length: 60 }, (_, i) => `https://site.test/${i}`);
147
- makeRequestSpy
148
- .mockResolvedValueOnce({ data: {} })
149
- .mockRejectedValueOnce(new Error('chunk failed'));
150
-
151
- await expect(oxy.getLinkPreviews(urls)).rejects.toThrow('chunk failed');
152
- });
153
- });
154
- });