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.
- package/.claude/commands/discuss.md +4 -0
- package/.github/workflows/ci.yml +2 -2
- package/README.md +2 -2
- package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
- package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
- package/dist/__tests__/e2e/pdf-assertions.js +9 -3
- package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
- package/dist/fingerprint.d.ts +12 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +196 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/image-builder.d.ts +4 -2
- package/dist/image-builder.d.ts.map +1 -1
- package/dist/image-builder.js +12 -3
- package/dist/image-builder.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +75 -8
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +179 -21
- package/dist/models.js.map +1 -1
- package/dist/page-builder.d.ts +24 -0
- package/dist/page-builder.d.ts.map +1 -0
- package/dist/page-builder.js +107 -0
- package/dist/page-builder.js.map +1 -0
- package/dist/paragraph-builder.d.ts +48 -54
- package/dist/paragraph-builder.d.ts.map +1 -1
- package/dist/paragraph-builder.js +408 -135
- package/dist/paragraph-builder.js.map +1 -1
- package/dist/pdfdancer_v1.d.ts +90 -9
- package/dist/pdfdancer_v1.d.ts.map +1 -1
- package/dist/pdfdancer_v1.js +559 -55
- package/dist/pdfdancer_v1.js.map +1 -1
- package/dist/types.d.ts +24 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +117 -2
- package/dist/types.js.map +1 -1
- package/docs/openapi.yml +2076 -0
- package/fixtures/Showcase.pdf +0 -0
- package/jest.config.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/e2e/acroform.test.ts +5 -5
- package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
- package/src/__tests__/e2e/form_x_object.test.ts +1 -1
- package/src/__tests__/e2e/image-showcase.test.ts +133 -0
- package/src/__tests__/e2e/image.test.ts +1 -1
- package/src/__tests__/e2e/line-showcase.test.ts +118 -0
- package/src/__tests__/e2e/line.test.ts +1 -16
- package/src/__tests__/e2e/page-showcase.test.ts +154 -0
- package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
- package/src/__tests__/e2e/paragraph.test.ts +8 -8
- package/src/__tests__/e2e/pdf-assertions.ts +10 -3
- package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
- package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
- package/src/__tests__/e2e/snapshot.test.ts +296 -0
- package/src/__tests__/e2e/token_from_env.test.ts +85 -25
- package/src/__tests__/fingerprint.test.ts +36 -0
- package/src/fingerprint.ts +169 -0
- package/src/image-builder.ts +13 -6
- package/src/index.ts +6 -1
- package/src/models.ts +208 -24
- package/src/page-builder.ts +130 -0
- package/src/paragraph-builder.ts +517 -159
- package/src/pdfdancer_v1.ts +662 -58
- package/src/types.ts +145 -2
- 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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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('
|
|
46
|
+
test('requires token from env or argument', async () => {
|
|
19
47
|
delete process.env.PDFDANCER_TOKEN;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
package/src/image-builder.ts
CHANGED
|
@@ -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(
|
|
37
|
-
|
|
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";
|