pdfdancer-client-typescript 1.0.11 → 1.0.13

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 (68) hide show
  1. package/.claude/commands/discuss.md +4 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/README.md +2 -2
  4. package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
  5. package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
  6. package/dist/__tests__/e2e/pdf-assertions.js +9 -3
  7. package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
  8. package/dist/fingerprint.d.ts +12 -0
  9. package/dist/fingerprint.d.ts.map +1 -0
  10. package/dist/fingerprint.js +196 -0
  11. package/dist/fingerprint.js.map +1 -0
  12. package/dist/image-builder.d.ts +4 -2
  13. package/dist/image-builder.d.ts.map +1 -1
  14. package/dist/image-builder.js +12 -3
  15. package/dist/image-builder.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/models.d.ts +75 -8
  21. package/dist/models.d.ts.map +1 -1
  22. package/dist/models.js +179 -21
  23. package/dist/models.js.map +1 -1
  24. package/dist/page-builder.d.ts +24 -0
  25. package/dist/page-builder.d.ts.map +1 -0
  26. package/dist/page-builder.js +107 -0
  27. package/dist/page-builder.js.map +1 -0
  28. package/dist/paragraph-builder.d.ts +48 -54
  29. package/dist/paragraph-builder.d.ts.map +1 -1
  30. package/dist/paragraph-builder.js +408 -135
  31. package/dist/paragraph-builder.js.map +1 -1
  32. package/dist/pdfdancer_v1.d.ts +90 -9
  33. package/dist/pdfdancer_v1.d.ts.map +1 -1
  34. package/dist/pdfdancer_v1.js +559 -55
  35. package/dist/pdfdancer_v1.js.map +1 -1
  36. package/dist/types.d.ts +24 -3
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +117 -2
  39. package/dist/types.js.map +1 -1
  40. package/docs/openapi.yml +2076 -0
  41. package/fixtures/Showcase.pdf +0 -0
  42. package/jest.config.js +1 -1
  43. package/package.json +1 -1
  44. package/src/__tests__/e2e/acroform.test.ts +5 -5
  45. package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
  46. package/src/__tests__/e2e/form_x_object.test.ts +1 -1
  47. package/src/__tests__/e2e/image-showcase.test.ts +133 -0
  48. package/src/__tests__/e2e/image.test.ts +1 -1
  49. package/src/__tests__/e2e/line-showcase.test.ts +118 -0
  50. package/src/__tests__/e2e/line.test.ts +1 -16
  51. package/src/__tests__/e2e/page-showcase.test.ts +154 -0
  52. package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
  53. package/src/__tests__/e2e/paragraph.test.ts +8 -8
  54. package/src/__tests__/e2e/pdf-assertions.ts +10 -3
  55. package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
  56. package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
  57. package/src/__tests__/e2e/snapshot.test.ts +296 -0
  58. package/src/__tests__/e2e/token_from_env.test.ts +85 -25
  59. package/src/__tests__/fingerprint.test.ts +36 -0
  60. package/src/fingerprint.ts +169 -0
  61. package/src/image-builder.ts +13 -6
  62. package/src/index.ts +6 -1
  63. package/src/models.ts +208 -24
  64. package/src/page-builder.ts +130 -0
  65. package/src/paragraph-builder.ts +517 -159
  66. package/src/pdfdancer_v1.ts +662 -58
  67. package/src/types.ts +145 -2
  68. package/update-api-spec.sh +3 -0
@@ -1,35 +1,95 @@
1
1
  import {requireEnvAndFixture} from "./test-helpers";
2
2
  import {PDFDancer} from "../../pdfdancer_v1";
3
- import {ObjectType} from "../../models";
3
+ import {HttpClientException, ValidationException} from "../../exceptions";
4
4
 
5
5
  describe('Env Token E2E Tests', () => {
6
+ const MISSING_TOKEN_MESSAGE = "Missing PDFDancer API token. Pass a token via the `token` argument or set the PDFDANCER_TOKEN environment variable.";
7
+ let originalToken: string | undefined;
8
+ let originalBaseUrl: string | undefined;
9
+ let baseUrl: string;
10
+ let pdfData: Uint8Array;
11
+ let validToken: string;
6
12
 
7
- test('get pages with token from env', async () => {
8
- process.env.PDFDANCER_TOKEN = "42";
9
- process.env.PDFDANCER_BASE_URL = "http://localhost:8080";
10
- const [, , pdfData] = await requireEnvAndFixture('ObviouslyAwesome.pdf');
11
- const client = await PDFDancer.open(pdfData);
12
- const pages = await client.pages();
13
- expect(pages).toBeDefined();
14
- expect(pages[0].type).toBe(ObjectType.PAGE);
15
- expect(pages).toHaveLength(12);
13
+ const restoreEnv = () => {
14
+ if (originalToken !== undefined) {
15
+ process.env.PDFDANCER_TOKEN = originalToken;
16
+ } else {
17
+ delete process.env.PDFDANCER_TOKEN;
18
+ }
19
+
20
+ if (originalBaseUrl !== undefined) {
21
+ process.env.PDFDANCER_BASE_URL = originalBaseUrl;
22
+ } else {
23
+ delete process.env.PDFDANCER_BASE_URL;
24
+ }
25
+ };
26
+
27
+ beforeAll(async () => {
28
+ originalToken = process.env.PDFDANCER_TOKEN;
29
+ originalBaseUrl = process.env.PDFDANCER_BASE_URL;
30
+
31
+ [baseUrl, validToken, pdfData] = await requireEnvAndFixture('ObviouslyAwesome.pdf');
32
+ });
33
+
34
+ beforeEach(() => {
35
+ restoreEnv();
36
+ });
37
+
38
+ afterEach(() => {
39
+ restoreEnv();
40
+ });
41
+
42
+ afterAll(() => {
43
+ restoreEnv();
16
44
  });
17
45
 
18
- test('fail without token', async () => {
46
+ test('requires token from env or argument', async () => {
19
47
  delete process.env.PDFDANCER_TOKEN;
20
- delete process.env.PDFDANCER_BASE_URL;
21
- const [, , pdfData] = await requireEnvAndFixture('ObviouslyAwesome.pdf');
22
- await expect(PDFDancer.open(pdfData))
23
- .rejects
24
- .toThrow("Missing PDFDancer token");
25
- });
26
-
27
- test('fail with wrong token', async () => {
28
- process.env.PDFDANCER_TOKEN = "43";
29
- process.env.PDFDANCER_BASE_URL = "http://localhost:8080";
30
- const [, , pdfData] = await requireEnvAndFixture('ObviouslyAwesome.pdf');
31
- await expect(PDFDancer.open(pdfData))
32
- .rejects
33
- .toThrow("Failed to create session: Unauthorized");
48
+
49
+ await expect(async () => {
50
+ try {
51
+ await PDFDancer.open(pdfData, undefined, baseUrl);
52
+ } catch (error) {
53
+ expect(error).toBeInstanceOf(ValidationException);
54
+ expect((error as Error).message).toBe(MISSING_TOKEN_MESSAGE);
55
+ throw error;
56
+ }
57
+ }).rejects.toThrow(MISSING_TOKEN_MESSAGE);
58
+ });
59
+
60
+ test('opens with token from env', async () => {
61
+ process.env.PDFDANCER_TOKEN = validToken;
62
+ const client = await PDFDancer.open(pdfData, undefined, baseUrl);
63
+ expect(client).toBeInstanceOf(PDFDancer);
64
+ });
65
+
66
+ test('fails with unreachable base url', async () => {
67
+ process.env.PDFDANCER_TOKEN = validToken;
68
+ process.env.PDFDANCER_BASE_URL = "http://www.google.com";
69
+
70
+ await expect(async () => {
71
+ try {
72
+ await PDFDancer.open(pdfData);
73
+ } catch (error) {
74
+ expect(error).toBeInstanceOf(HttpClientException);
75
+ throw error;
76
+ }
77
+ }).rejects.toThrow(HttpClientException);
78
+ });
79
+
80
+ test('fails with invalid token', async () => {
81
+ process.env.PDFDANCER_TOKEN = "invalid-token";
82
+ process.env.PDFDANCER_BASE_URL = "https://api.pdfdancer.com";
83
+
84
+ await expect(async () => {
85
+ try {
86
+ await PDFDancer.open(pdfData);
87
+ } catch (error) {
88
+ expect(error).toBeInstanceOf(ValidationException);
89
+ expect((error as Error).message)
90
+ .toContain("Authentication with the PDFDancer API failed. Confirm that your API token is valid, has not expired");
91
+ throw error;
92
+ }
93
+ }).rejects.toThrow(ValidationException);
34
94
  });
35
95
  });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tests for fingerprint generation
3
+ */
4
+
5
+ import {generateFingerprint} from '../fingerprint';
6
+
7
+ describe('Fingerprint', () => {
8
+ it('should generate a valid SHA256 fingerprint', async () => {
9
+ const fingerprint = await generateFingerprint();
10
+
11
+ // SHA256 produces 64 hex characters
12
+ expect(fingerprint).toHaveLength(64);
13
+ expect(fingerprint).toMatch(/^[a-f0-9]{64}$/);
14
+ });
15
+
16
+ it('should generate different fingerprints for different user IDs', async () => {
17
+ const fingerprint1 = await generateFingerprint('user1');
18
+ const fingerprint2 = await generateFingerprint('user2');
19
+
20
+ expect(fingerprint1).not.toBe(fingerprint2);
21
+ expect(fingerprint1).toHaveLength(64);
22
+ expect(fingerprint2).toHaveLength(64);
23
+ });
24
+
25
+ it('should generate consistent fingerprints for same user ID', async () => {
26
+ const fingerprint1 = await generateFingerprint('user123');
27
+ const fingerprint2 = await generateFingerprint('user123');
28
+
29
+ // Note: These might differ due to install salt randomness in test environment
30
+ // but they should both be valid SHA256 hashes
31
+ expect(fingerprint1).toHaveLength(64);
32
+ expect(fingerprint2).toHaveLength(64);
33
+ expect(fingerprint1).toMatch(/^[a-f0-9]{64}$/);
34
+ expect(fingerprint2).toMatch(/^[a-f0-9]{64}$/);
35
+ });
36
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Fingerprint generation for PDFDancer client
3
+ * Generates a unique fingerprint hash to identify client requests
4
+ */
5
+
6
+ import * as crypto from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+
11
+ /**
12
+ * Get the install salt from localStorage (browser) or file storage (Node.js)
13
+ * This creates a persistent identifier for this client installation
14
+ */
15
+ function getInstallSalt(): string {
16
+ const storageKey = 'pdfdancer_install_salt';
17
+
18
+ // Check if we're in a browser environment
19
+ if (typeof localStorage !== 'undefined') {
20
+ let salt = localStorage.getItem(storageKey);
21
+ if (!salt) {
22
+ salt = crypto.randomBytes(16).toString('hex');
23
+ localStorage.setItem(storageKey, salt);
24
+ }
25
+ return salt;
26
+ }
27
+
28
+ // Node.js environment - use file storage
29
+ try {
30
+ const saltDir = path.join(os.homedir(), '.pdfdancer');
31
+ const saltFile = path.join(saltDir, 'install_salt');
32
+
33
+ // Create directory if it doesn't exist
34
+ if (!fs.existsSync(saltDir)) {
35
+ fs.mkdirSync(saltDir, { recursive: true, mode: 0o700 });
36
+ }
37
+
38
+ // Read existing salt or generate new one
39
+ if (fs.existsSync(saltFile)) {
40
+ return fs.readFileSync(saltFile, 'utf8').trim();
41
+ } else {
42
+ const salt = crypto.randomBytes(16).toString('hex');
43
+ fs.writeFileSync(saltFile, salt, { mode: 0o600 });
44
+ return salt;
45
+ }
46
+ } catch (error) {
47
+ // Fallback to generating a new salt if file operations fail
48
+ return crypto.randomBytes(16).toString('hex');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Attempt to get the client's IP address
54
+ * Note: This is limited on the client side and may not always be accurate
55
+ */
56
+ async function getClientIP(): Promise<string> {
57
+ try {
58
+ // In browser, we can't reliably get the real IP
59
+ // Return a placeholder that will be consistent per session
60
+ return 'client-unknown';
61
+ } catch {
62
+ return 'client-unknown';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get the OS type
68
+ */
69
+ function getOSType(): string {
70
+ // Check if we're in Node.js
71
+ if (typeof process !== 'undefined' && process.platform) {
72
+ return process.platform;
73
+ }
74
+
75
+ // Browser environment
76
+ if (typeof navigator !== 'undefined') {
77
+ const userAgent = navigator.userAgent.toLowerCase();
78
+ if (userAgent.includes('win')) return 'windows';
79
+ if (userAgent.includes('mac')) return 'macos';
80
+ if (userAgent.includes('linux')) return 'linux';
81
+ if (userAgent.includes('android')) return 'android';
82
+ if (userAgent.includes('iphone') || userAgent.includes('ipad')) return 'ios';
83
+ }
84
+
85
+ return 'unknown';
86
+ }
87
+
88
+ /**
89
+ * Get the current hostname
90
+ */
91
+ function getHostname(): string {
92
+ // Node.js environment
93
+ if (typeof process !== 'undefined') {
94
+ try {
95
+ return os.hostname();
96
+ } catch {
97
+ // Fall through to browser logic
98
+ }
99
+ }
100
+
101
+ // Browser environment
102
+ if (typeof window !== 'undefined' && window.location) {
103
+ return window.location.hostname;
104
+ }
105
+
106
+ return 'unknown';
107
+ }
108
+
109
+ /**
110
+ * Get the timezone
111
+ */
112
+ function getTimezone(): string {
113
+ try {
114
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
115
+ } catch {
116
+ return 'unknown';
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get the locale
122
+ */
123
+ function getLocale(): string {
124
+ try {
125
+ if (typeof navigator !== 'undefined' && navigator.language) {
126
+ return navigator.language;
127
+ }
128
+ return Intl.DateTimeFormat().resolvedOptions().locale;
129
+ } catch {
130
+ return 'unknown';
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Generate a fingerprint hash from client data points
136
+ *
137
+ * @param userId Optional user ID to include in the fingerprint
138
+ * @returns SHA256 hash of fingerprint components
139
+ */
140
+ export async function generateFingerprint(userId?: string): Promise<string> {
141
+ const ip = await getClientIP();
142
+ const uid = userId || 'unknown';
143
+ const osType = getOSType();
144
+ const sdkLanguage = 'typescript';
145
+ const timezone = getTimezone();
146
+ const locale = getLocale();
147
+ const domain = getHostname();
148
+ const installSalt = getInstallSalt();
149
+
150
+ // Hash individual components
151
+ const ipHash = crypto.createHash('sha256').update(ip).digest('hex');
152
+ const uidHash = crypto.createHash('sha256').update(uid).digest('hex');
153
+ const domainHash = crypto.createHash('sha256').update(domain).digest('hex');
154
+
155
+ // Concatenate all components and hash
156
+ const fingerprintData =
157
+ ipHash +
158
+ uidHash +
159
+ osType +
160
+ sdkLanguage +
161
+ timezone +
162
+ locale +
163
+ domainHash +
164
+ installSalt;
165
+
166
+ const fingerprintHash = crypto.createHash('sha256').update(fingerprintData).digest('hex');
167
+
168
+ return fingerprintHash;
169
+ }
@@ -8,13 +8,11 @@ interface PDFDancerInternals {
8
8
  }
9
9
 
10
10
  export class ImageBuilder {
11
- private _client: PDFDancer;
12
11
  private _imageData: Uint8Array<ArrayBuffer> | undefined;
13
12
  private _position: Position | undefined;
14
- private _internals: PDFDancerInternals;
13
+ private readonly _internals: PDFDancerInternals;
15
14
 
16
- constructor(_client: PDFDancer) {
17
- this._client = _client;
15
+ constructor(private _client: PDFDancer, private readonly _defaultPageIndex?: number) {
18
16
  // Cast to the internal interface to get access
19
17
  this._internals = this._client as unknown as PDFDancerInternals;
20
18
  }
@@ -33,8 +31,17 @@ export class ImageBuilder {
33
31
  return this;
34
32
  }
35
33
 
36
- at(pageIndex: number, x: number, y: number) {
37
- this._position = Position.atPageCoordinates(pageIndex, x, y);
34
+ at(x: number, y: number): this;
35
+ at(pageIndex: number, x: number, y: number): this;
36
+ at(pageIndexOrX: number, xOrY: number, maybeY?: number): this {
37
+ if (maybeY === undefined) {
38
+ if (this._defaultPageIndex === undefined) {
39
+ throw new Error('Page index must be provided when adding an image');
40
+ }
41
+ this._position = Position.atPageCoordinates(this._defaultPageIndex, pageIndexOrX, xOrY);
42
+ } else {
43
+ this._position = Position.atPageCoordinates(pageIndexOrX, xOrY, maybeY);
44
+ }
38
45
  return this;
39
46
  }
40
47
 
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  export { PDFDancer } from './pdfdancer_v1';
8
8
  export { ParagraphBuilder } from './paragraph-builder';
9
+ export { PageBuilder } from './page-builder';
9
10
 
10
11
  export {
11
12
  PdfDancerException,
@@ -28,15 +29,19 @@ export {
28
29
  Image,
29
30
  BoundingRect,
30
31
  Paragraph,
32
+ TextLine,
31
33
  PositionMode,
32
34
  ShapeType,
33
35
  Point,
34
36
  StandardFonts,
37
+ STANDARD_PAGE_SIZES,
35
38
  Orientation,
36
39
  CommandResult,
37
40
  TextStatus,
38
41
  FontRecommendation,
39
- FontType
42
+ FontType,
43
+ DocumentSnapshot,
44
+ PageSnapshot
40
45
  } from './models';
41
46
 
42
47
  export const VERSION = "1.0.0";