@getsupertab/supertab-connect-sdk 0.1.0-beta.18
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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Supertab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Supertab Connect SDK
|
|
2
|
+
|
|
3
|
+
Check our [documentation](https://supertab-connect.mintlify.app/introduction/about-supertab-connect) for more information on Supertab Connect.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @getsupertab/supertab-connect-sdk
|
|
9
|
+
# or
|
|
10
|
+
yarn add @getsupertab/supertab-connect-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
### Manual setup
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { SupertabConnect } from "@getsupertab/supertab-connect-sdk";
|
|
19
|
+
|
|
20
|
+
// Initialize the SDK
|
|
21
|
+
const supertabConnect = new SupertabConnect({
|
|
22
|
+
apiKey: "stc_live_your_api_key",
|
|
23
|
+
merchantSystemUrn: "your_merchant_system_urn",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Verify a token
|
|
27
|
+
const verification = await supertabConnect.verifyToken(token);
|
|
28
|
+
|
|
29
|
+
// Record an event
|
|
30
|
+
await supertabConnect.recordEvent("page_viewed", token, {
|
|
31
|
+
page_url: "https://example.com/article",
|
|
32
|
+
user_agent: "Mozilla/5.0...",
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Fastly Compute Example
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
/// <reference types="@fastly/js-compute" />
|
|
40
|
+
import { SecretStore } from "fastly:secret-store";
|
|
41
|
+
import { SupertabConnect } from "@getsupertab/supertab-connect-sdk";
|
|
42
|
+
|
|
43
|
+
const configDict = new SecretStore("demo");
|
|
44
|
+
const config = {
|
|
45
|
+
apiKey: configDict.get("MERCHANT_API_KEY"),
|
|
46
|
+
merchantSystemUrn: configDict.get("MERCHANT_SYSTEM_URN"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// The entry point for the request handler.
|
|
50
|
+
addEventListener("fetch", (event) =>
|
|
51
|
+
event.respondWith(
|
|
52
|
+
SupertabConnect.fastlyHandleRequests(
|
|
53
|
+
event.request,
|
|
54
|
+
config.merchantSystemUrn,
|
|
55
|
+
config.apiKey
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### CloudFlare Worker Example
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { SupertabConnect, Env } from "@getsupertab/supertab-connect-sdk";
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
async fetch(
|
|
68
|
+
request: Request,
|
|
69
|
+
env: Env,
|
|
70
|
+
ctx: ExecutionContext
|
|
71
|
+
): Promise<Response> {
|
|
72
|
+
return SupertabConnect.cloudflareHandleRequests(request, env, ctx);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration Options
|
|
78
|
+
|
|
79
|
+
The SDK is configured using the `SupertabConnectConfig` object
|
|
80
|
+
|
|
81
|
+
| Parameter | Type | Required | Default | Description |
|
|
82
|
+
| ------------------- | ------ | -------- | ------- | ------------------------------- |
|
|
83
|
+
| `apiKey` | string | Yes | - | Your Supertab merchant API key |
|
|
84
|
+
| `merchantSystemUrn` | string | Yes | - | Your merchant system identifier |
|
|
85
|
+
|
|
86
|
+
## Public API Reference
|
|
87
|
+
|
|
88
|
+
### `constructor(config: SupertabConnectConfig, reset: boolean = false)`
|
|
89
|
+
|
|
90
|
+
Creates a new instance of the SupertabConnect SDK.
|
|
91
|
+
If the SDK was already initialized and the config parameters are different, it will throw an error unless `reset` is set to true.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { SupertabConnect } from "@getsupertab/supertab-connect-sdk";
|
|
95
|
+
|
|
96
|
+
const supertabConnect = new SupertabConnect({
|
|
97
|
+
apiKey: "stc_live_your_api_key",
|
|
98
|
+
merchantSystemUrn: "your_merchant_system_urn",
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `resetInstance(): void`
|
|
103
|
+
|
|
104
|
+
Resets the singleton instance of SupertabConnect allowing to create an instance with a new config.
|
|
105
|
+
We expect this to not be called in the usual production setup as the SDK is designed to intercept requests using specific public methods.
|
|
106
|
+
|
|
107
|
+
### `fastlyHandleRequests(request: Request, merchantSystemUrn: string, merchantApiKey: string): Promise<Response>`
|
|
108
|
+
|
|
109
|
+
Handles the Supertab Connect part for each incoming HTTP request within Fastly CDN: it verifies the JWT token and records the event.
|
|
110
|
+
|
|
111
|
+
For examples see the [Fastly Compute Example](#fastly-compute-example) section above.
|
|
112
|
+
|
|
113
|
+
**Parameters:**
|
|
114
|
+
|
|
115
|
+
- `request` (Request): The incoming HTTP request object
|
|
116
|
+
- `merchantSystemUrn` (string): Your merchant system identifier (recommended to be stored in a Fastly SecretStore)
|
|
117
|
+
- `merchantApiKey` (string): Your Supertab merchant API key (recommended to be stored in a Fastly SecretStore)
|
|
118
|
+
|
|
119
|
+
**Returns:**
|
|
120
|
+
|
|
121
|
+
- `Promise<Response>`: Result of bot detection, verification and event recording
|
|
122
|
+
- If the requester is not a bot to be blocked, or if the token is present and valid, returns 200 OK
|
|
123
|
+
- If token is invalid or missing, returns 403 Forbidden with either INVALID_TOKEN or MISSING_TOKEN as a reason
|
|
124
|
+
|
|
125
|
+
### `cloudflareHandleRequests(request: Request, env: Env, ctx: any = null): Promise<Response>`
|
|
126
|
+
|
|
127
|
+
Handles the Supertab Connect part for each incoming HTTP request within CloudFlare CDN: it verifies the JWT token and records the event.
|
|
128
|
+
|
|
129
|
+
For examples see the [CloudFlare Worker Example](#cloudflare-worker-example) section above.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
- `request` (Request): The incoming HTTP request object
|
|
134
|
+
- `env` (Env): Environment variables provided for CloudFlare Workers (MERCHANT_API_KEY and MERCHANT_SYSTEM_URN must be present)
|
|
135
|
+
- `ctx` (ExecutionContext): Execution context passed from `fetch` worker method for awaiting async operations
|
|
136
|
+
|
|
137
|
+
**Returns:**
|
|
138
|
+
|
|
139
|
+
- `Promise<Response>`: Result of bot detection, verification and event recording
|
|
140
|
+
- If the requester is not a bot to be blocked, or if the token is present and valid, returns 200 OK
|
|
141
|
+
- If token is invalid or missing, returns 403 Forbidden with either INVALID_TOKEN or MISSING_TOKEN as a reason
|
|
142
|
+
|
|
143
|
+
### `handleRequest(request: Request, botDetectionHandler?: (request: Request, ctx?: any) => boolean, ctx?: any): Promise<Response>`
|
|
144
|
+
|
|
145
|
+
A method for handling requests in a generic way, allowing custom bot detection logic.
|
|
146
|
+
Any of out-of-the-box bot detector methods can be used or a custom one can be supplied provided it follows the specified signature.
|
|
147
|
+
|
|
148
|
+
**Parameters:**
|
|
149
|
+
|
|
150
|
+
- `request` (Request): The incoming HTTP request object
|
|
151
|
+
- `botDetectionHandler` (function, optional): Custom function to detect bots. It should return a boolean indicating if the request is from a bot.
|
|
152
|
+
- `ctx` (ExecutionContext, optional): Context object to for awaiting async operations
|
|
153
|
+
|
|
154
|
+
**Returns:**
|
|
155
|
+
|
|
156
|
+
- `Promise<Response>`: Result of bot detection, verification and event recording
|
|
157
|
+
- If the requester is not a bot to be blocked, or if the token is present and valid, returns 200 OK
|
|
158
|
+
- If token is invalid or missing, returns 403 Forbidden with either INVALID_TOKEN or MISSING_TOKEN as a reason
|
|
159
|
+
|
|
160
|
+
### `verifyToken(token: string): Promise<TokenVerificationResult>`
|
|
161
|
+
|
|
162
|
+
Verifies self-signed JWT Tokens sent by the Customer and signed by their private-key. Internally, it fetches the JWKs hosted by Supertab Connect for the customer and verifies using the public key available.
|
|
163
|
+
|
|
164
|
+
**Parameters:**
|
|
165
|
+
|
|
166
|
+
- `token` (string): The JWT token to verify
|
|
167
|
+
|
|
168
|
+
**Returns:**
|
|
169
|
+
|
|
170
|
+
- `Promise<TokenVerificationResult>`: Object with verification result
|
|
171
|
+
- `valid`: boolean indicating if token is valid
|
|
172
|
+
- `reason`: string reason for failure (if invalid)
|
|
173
|
+
- `payload`: decoded token payload (if valid)
|
|
174
|
+
|
|
175
|
+
Example
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const token = "eyJhbGciOiJSUzI1..."; // Token from Authorization header
|
|
179
|
+
const verification = await supertabConnect.verifyToken(token);
|
|
180
|
+
|
|
181
|
+
if (verification.valid) {
|
|
182
|
+
// Allow access to content
|
|
183
|
+
console.log("Token verified successfully", verification.payload);
|
|
184
|
+
} else {
|
|
185
|
+
// Block access
|
|
186
|
+
console.log("Token verification failed:", verification.reason);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `recordEvent(eventName: string, customerToken?: string, properties?: Record<string, any>): Promise<void>`
|
|
191
|
+
|
|
192
|
+
Records an event in the Supertab Connect platform.
|
|
193
|
+
|
|
194
|
+
**Parameters:**
|
|
195
|
+
|
|
196
|
+
- `eventName` (string): Name of the event to record
|
|
197
|
+
- `customerToken` (string, optional): The self-signed JWT token sent by the customer
|
|
198
|
+
- `properties` (object, optional): Additional properties to include with the event
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// Record a page view with additional properties
|
|
204
|
+
await supertabConnect.recordEvent("page_viewed", token, {
|
|
205
|
+
page_url: request.url,
|
|
206
|
+
user_agent: request.headers.get("User-Agent"),
|
|
207
|
+
referrer: request.headers.get("Referer"),
|
|
208
|
+
article_id: "12345",
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `checkIfBotRequest(request: Request): boolean`
|
|
213
|
+
|
|
214
|
+
A simple check based on the information passed in request's Headers to decide whether it comes from a crawler/bot/AI agent or not.
|
|
215
|
+
|
|
216
|
+
**Parameters:**
|
|
217
|
+
|
|
218
|
+
- `request` (Request): The incoming HTTP request object
|
|
219
|
+
|
|
220
|
+
**Returns:**
|
|
221
|
+
|
|
222
|
+
- `boolean`: True if the request's Headers fall into the conditions to identify requester as a bot; False otherwise.
|
|
223
|
+
|
|
224
|
+
### `generateCustomerJWT(customerURN: string, kid: string, privateKeyPem: string, expirationSeconds?: number): Promise<string>`
|
|
225
|
+
|
|
226
|
+
Generates a self‑signed RS256 JWT for a Customer with a `kid` header. The `privateKeyPem` must be in PEM format.
|
|
227
|
+
|
|
228
|
+
**Parameters:**
|
|
229
|
+
|
|
230
|
+
- `customerURN` (string): The Customer URN (must start with `urn:stc:customer:`)
|
|
231
|
+
- `kid` (string): Key ID to include in the JWT header
|
|
232
|
+
- `privateKeyPem` (string): the private key in PEM format
|
|
233
|
+
- `expirationSeconds` (number, optional): Expiry in seconds (default: `3600`)
|
|
234
|
+
|
|
235
|
+
**Returns:**
|
|
236
|
+
|
|
237
|
+
- `Promise<string>`: The signed JWT
|
|
238
|
+
|
|
239
|
+
**Example:**
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { SupertabConnect } from "@getsupertab/supertab-connect-sdk";
|
|
243
|
+
|
|
244
|
+
const token = await SupertabConnect.generateCustomerJWT(
|
|
245
|
+
"urn:stc:customer:79fcac58-1966-470a-be40-c34847aecabd",
|
|
246
|
+
"key_dc0d3d1103c319e9",
|
|
247
|
+
`-----BEGIN PRIVATE KEY-----
|
|
248
|
+
...
|
|
249
|
+
-----END PRIVATE KEY-----`,
|
|
250
|
+
3600
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
console.log("Generated JWT:", token);
|
|
254
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
interface SupertabConnectConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
merchantSystemUrn: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Defines the shape for environment variables (used in CloudFlare integration).
|
|
7
|
+
* These are used to identify and authenticate the Merchant System with the Supertab Connect API.
|
|
8
|
+
*/
|
|
9
|
+
interface Env {
|
|
10
|
+
/** The unique identifier for the merchant system. */
|
|
11
|
+
MERCHANT_SYSTEM_URN: string;
|
|
12
|
+
/** The API key for authenticating with the Supertab Connect. */
|
|
13
|
+
MERCHANT_API_KEY: string;
|
|
14
|
+
[key: string]: string;
|
|
15
|
+
}
|
|
16
|
+
interface TokenVerificationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
payload?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SupertabConnect class provides higher level methods
|
|
24
|
+
* for using Supertab Connect within supported CDN integrations
|
|
25
|
+
* as well as more specialized methods to customarily verify JWT tokens and record events.
|
|
26
|
+
*/
|
|
27
|
+
declare class SupertabConnect {
|
|
28
|
+
private apiKey?;
|
|
29
|
+
private baseUrl?;
|
|
30
|
+
private merchantSystemUrn?;
|
|
31
|
+
private static _instance;
|
|
32
|
+
constructor(config: SupertabConnectConfig, reset?: boolean);
|
|
33
|
+
static resetInstance(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get the JWKS for a given issuer, using cache if available
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
private getJwksForIssuer;
|
|
39
|
+
/**
|
|
40
|
+
* Verify a JWT token
|
|
41
|
+
* @param token The JWT token to verify
|
|
42
|
+
* @returns A promise that resolves with the verification result
|
|
43
|
+
*/
|
|
44
|
+
verifyToken(token: string): Promise<TokenVerificationResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Records an analytics event
|
|
47
|
+
* @param eventName Name of the event to record
|
|
48
|
+
* @param customerToken Optional customer token for the event
|
|
49
|
+
* @param properties Additional properties to include with the event
|
|
50
|
+
* @returns Promise that resolves when the event is recorded
|
|
51
|
+
*/
|
|
52
|
+
recordEvent(eventName: string, customerToken?: string, properties?: Record<string, any>): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Handle the request, report an event to Supertab Connect and return a response
|
|
55
|
+
*/
|
|
56
|
+
private baseHandleRequest;
|
|
57
|
+
private extractDataFromRequest;
|
|
58
|
+
static checkIfBotRequest(request: Request): boolean;
|
|
59
|
+
static cloudflareHandleRequests(request: Request, env: Env, ctx: any): Promise<Response>;
|
|
60
|
+
static fastlyHandleRequests(request: Request, merchantSystemUrn: string, merchantApiKey: string): Promise<Response>;
|
|
61
|
+
handleRequest(request: Request, botDetectionHandler?: (request: Request, ctx?: any) => boolean, ctx?: any): Promise<Response>;
|
|
62
|
+
/** Generate a customer JWT
|
|
63
|
+
* @param customerURN The customer's unique resource name (URN).
|
|
64
|
+
* @param kid The key ID to include in the JWT header.
|
|
65
|
+
* @param privateKeyPem The private key in PEM format used to sign the JWT.
|
|
66
|
+
* @param expirationSeconds The token's expiration time in seconds (default is 3600 seconds).
|
|
67
|
+
* @returns A promise that resolves to the generated JWT as a string.
|
|
68
|
+
*/
|
|
69
|
+
static generateCustomerJWT(customerURN: string, kid: string, privateKeyPem: string, expirationSeconds?: number): Promise<string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { type Env, SupertabConnect };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
interface SupertabConnectConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
merchantSystemUrn: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Defines the shape for environment variables (used in CloudFlare integration).
|
|
7
|
+
* These are used to identify and authenticate the Merchant System with the Supertab Connect API.
|
|
8
|
+
*/
|
|
9
|
+
interface Env {
|
|
10
|
+
/** The unique identifier for the merchant system. */
|
|
11
|
+
MERCHANT_SYSTEM_URN: string;
|
|
12
|
+
/** The API key for authenticating with the Supertab Connect. */
|
|
13
|
+
MERCHANT_API_KEY: string;
|
|
14
|
+
[key: string]: string;
|
|
15
|
+
}
|
|
16
|
+
interface TokenVerificationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
payload?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SupertabConnect class provides higher level methods
|
|
24
|
+
* for using Supertab Connect within supported CDN integrations
|
|
25
|
+
* as well as more specialized methods to customarily verify JWT tokens and record events.
|
|
26
|
+
*/
|
|
27
|
+
declare class SupertabConnect {
|
|
28
|
+
private apiKey?;
|
|
29
|
+
private baseUrl?;
|
|
30
|
+
private merchantSystemUrn?;
|
|
31
|
+
private static _instance;
|
|
32
|
+
constructor(config: SupertabConnectConfig, reset?: boolean);
|
|
33
|
+
static resetInstance(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get the JWKS for a given issuer, using cache if available
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
private getJwksForIssuer;
|
|
39
|
+
/**
|
|
40
|
+
* Verify a JWT token
|
|
41
|
+
* @param token The JWT token to verify
|
|
42
|
+
* @returns A promise that resolves with the verification result
|
|
43
|
+
*/
|
|
44
|
+
verifyToken(token: string): Promise<TokenVerificationResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Records an analytics event
|
|
47
|
+
* @param eventName Name of the event to record
|
|
48
|
+
* @param customerToken Optional customer token for the event
|
|
49
|
+
* @param properties Additional properties to include with the event
|
|
50
|
+
* @returns Promise that resolves when the event is recorded
|
|
51
|
+
*/
|
|
52
|
+
recordEvent(eventName: string, customerToken?: string, properties?: Record<string, any>): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Handle the request, report an event to Supertab Connect and return a response
|
|
55
|
+
*/
|
|
56
|
+
private baseHandleRequest;
|
|
57
|
+
private extractDataFromRequest;
|
|
58
|
+
static checkIfBotRequest(request: Request): boolean;
|
|
59
|
+
static cloudflareHandleRequests(request: Request, env: Env, ctx: any): Promise<Response>;
|
|
60
|
+
static fastlyHandleRequests(request: Request, merchantSystemUrn: string, merchantApiKey: string): Promise<Response>;
|
|
61
|
+
handleRequest(request: Request, botDetectionHandler?: (request: Request, ctx?: any) => boolean, ctx?: any): Promise<Response>;
|
|
62
|
+
/** Generate a customer JWT
|
|
63
|
+
* @param customerURN The customer's unique resource name (URN).
|
|
64
|
+
* @param kid The key ID to include in the JWT header.
|
|
65
|
+
* @param privateKeyPem The private key in PEM format used to sign the JWT.
|
|
66
|
+
* @param expirationSeconds The token's expiration time in seconds (default is 3600 seconds).
|
|
67
|
+
* @returns A promise that resolves to the generated JWT as a string.
|
|
68
|
+
*/
|
|
69
|
+
static generateCustomerJWT(customerURN: string, kid: string, privateKeyPem: string, expirationSeconds?: number): Promise<string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { type Env, SupertabConnect };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var g=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var b=(p,e)=>{for(var t in e)g(p,t,{get:e[t],enumerable:!0})},S=(p,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of E(e))!I.call(p,r)&&r!==t&&g(p,r,{get:()=>e[r],enumerable:!(s=R(e,r))||s.enumerable});return p};var A=p=>S(g({},"__esModule",{value:!0}),p);var C={};b(C,{SupertabConnect:()=>f});module.exports=A(C);var u=require("jose"),m=new Map,h=!0,a=class a{constructor(e,t=!1){if(!t&&a._instance){if(!(e.apiKey===a._instance.apiKey&&e.merchantSystemUrn===a._instance.merchantSystemUrn))throw new Error("Cannot create a new instance with different configuration. Use resetInstance to clear the existing instance.");return a._instance}if(t&&a._instance&&a.resetInstance(),!e.apiKey||!e.merchantSystemUrn)throw new Error("Missing required configuration: apiKey and merchantSystemUrn are required");this.apiKey=e.apiKey,this.merchantSystemUrn=e.merchantSystemUrn,this.baseUrl="https://api-connect.sbx.supertab.co",a._instance=this}static resetInstance(){a._instance=null}async getJwksForIssuer(e){if(!m.has(e)){let t=`${this.baseUrl}/.well-known/jwks.json/${encodeURIComponent(e)}`;try{let s=await fetch(t);if(!s.ok)throw new Error(`Failed to fetch JWKS: ${s.status}`);let r=await s.json();m.set(e,r)}catch(s){throw h&&console.error("Error fetching JWKS:",s),s}}return m.get(e)}async verifyToken(e){if(!e)return{valid:!1,reason:"missing_token"};let t;try{t=(0,u.decodeProtectedHeader)(e)}catch(n){return h&&console.error("Invalid JWT header:",n),{valid:!1,reason:"invalid_header"}}if(t.alg!=="RS256")return{valid:!1,reason:"invalid_algorithm"};let s;try{s=(0,u.decodeJwt)(e)}catch(n){return h&&console.error("Invalid JWT payload:",n),{valid:!1,reason:"invalid_payload"}}let r=s.iss;if(!r||!r.startsWith("urn:stc:customer:"))return{valid:!1,reason:"invalid_issuer"};try{let n=await this.getJwksForIssuer(r);return{valid:!0,payload:(await(0,u.jwtVerify)(e,async i=>{let d=n.keys.find(l=>l.kid===i.kid);if(!d)throw new Error(`No matching key found: ${i.kid}`);return d},{issuer:r,algorithms:["RS256"],clockTolerance:"1m"})).payload}}catch(n){return h&&console.error("JWT verification failed:",n),n.message?.includes("exp")?{valid:!1,reason:"token_expired"}:{valid:!1,reason:"signature_verification_failed"}}}async recordEvent(e,t,s={}){let r={event_name:e,customer_system_token:t,merchant_system_urn:this.merchantSystemUrn?this.merchantSystemUrn:"",properties:s};try{let n=await fetch(`${this.baseUrl}/events`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(r)});n.ok||console.log(`Failed to record event: ${n.status}`)}catch(n){console.log("Error recording event:",n)}}async baseHandleRequest(e,t,s,r){let n=await this.verifyToken(e);async function o(c,i,d){let l={page_url:t,user_agent:s,verification_status:n.valid?"valid":"invalid",verification_reason:n.reason||"success"};if(d){let y=c.recordEvent(i,e,l);return d.waitUntil(y),y}else return await c.recordEvent(i,e,l)}if(!n.valid){await o(this,n.reason||"token_verification_failed",r);let c="Payment required: you need to present a valid Supertab Connect token to access this content. Check out the provided url for details",i="\u274C Content access denied"+(n.reason?`: ${n.reason}`:""),l={url:`${this.baseUrl}/merchants/systems/${this.merchantSystemUrn}/content-access.json`,message:c,details:i};return new Response(JSON.stringify(l),{status:402,headers:new Headers({"Content-Type":"application/json"})})}return await o(this,"page_viewed",r),new Response("\u2705 Content Access granted",{status:200,headers:new Headers({"Content-Type":"application/json"})})}extractDataFromRequest(e){let t=e.headers.get("Authorization")||"",s=t.startsWith("Bearer ")?t.slice(7):"",r=e.url,n=e.headers.get("User-Agent")||"unknown";return{token:s,url:r,user_agent:n}}static checkIfBotRequest(e){let t=e.headers.get("User-Agent")||"",s=e.headers.get("accept")||"",r=e.headers.get("sec-ch-ua"),n=e.headers.get("accept-language"),o=e.cf?.botManagement?.score,c=["chatgpt-user","perplexitybot","gptbot","anthropic-ai","ccbot","claude-web","claudebot","cohere-ai","youbot","diffbot","oai-searchbot","meta-externalagent","timpibot","amazonbot","bytespider","perplexity-user","googlebot","bot","curl","wget"],i=t.toLowerCase(),d=c.some(_=>i.includes(_)),l=t.toLowerCase().includes("headless")||t.toLowerCase().includes("puppeteer")||!r,y=!t.toLowerCase().includes("headless")||!t.toLowerCase().includes("puppeteer")||!r,w=!s||!n,v=typeof o=="number"&&o<30;return console.log("Bot Detection Details:",{botUaMatch:d,headlessIndicators:l,missingHeaders:w,lowBotScore:v,botScore:o}),(i.includes("safari")||i.includes("mozilla"))&&l&&y?!1:d||l||w||v}static async cloudflareHandleRequests(e,t,s){let{MERCHANT_SYSTEM_URN:r,MERCHANT_API_KEY:n}=t;return new a({apiKey:n,merchantSystemUrn:r}).handleRequest(e,a.checkIfBotRequest,s)}static async fastlyHandleRequests(e,t,s){return new a({apiKey:s,merchantSystemUrn:t}).handleRequest(e,a.checkIfBotRequest,null)}async handleRequest(e,t,s){let{token:r,url:n,user_agent:o}=this.extractDataFromRequest(e);return t&&!t(e,s)?new Response("\u2705 Non-Bot Content Access granted",{status:200,headers:new Headers({"Content-Type":"application/json"})}):this.baseHandleRequest(r,n,o,s)}static async generateCustomerJWT(e,t,s,r=3600){let n="RS256",o=await(0,u.importPKCS8)(s,n),c=Math.floor(Date.now()/1e3);return new u.SignJWT({}).setProtectedHeader({alg:n,kid:t}).setIssuer(e).setIssuedAt(c).setExpirationTime(c+r).sign(o)}};a._instance=null;var f=a;0&&(module.exports={SupertabConnect});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n SupertabConnectConfig,\n Env,\n EventPayload,\n TokenVerificationResult,\n TokenInvalidReason,\n} from \"./types\";\nimport {\n jwtVerify,\n decodeProtectedHeader,\n decodeJwt,\n JWTHeaderParameters,\n JWTPayload,\n importPKCS8,\n SignJWT,\n} from \"jose\";\n\nexport type { Env } from \"./types\";\n\n// In-memory cache for JWK sets\nconst jwksCache = new Map<string, any>();\nconst debug = true; // Set to true for debugging purposes\n\n/**\n * SupertabConnect class provides higher level methods\n * for using Supertab Connect within supported CDN integrations\n * as well as more specialized methods to customarily verify JWT tokens and record events.\n */\nexport class SupertabConnect {\n private apiKey?: string;\n private baseUrl?: string;\n private merchantSystemUrn?: string;\n\n private static _instance: SupertabConnect | null = null;\n\n public constructor(config: SupertabConnectConfig, reset: boolean = false) {\n if (!reset && SupertabConnect._instance) {\n // If reset was not requested and an instance conflicts with the provided config, throw an error\n if (\n !(\n config.apiKey === SupertabConnect._instance.apiKey &&\n config.merchantSystemUrn ===\n SupertabConnect._instance.merchantSystemUrn\n )\n ) {\n throw new Error(\n \"Cannot create a new instance with different configuration. Use resetInstance to clear the existing instance.\"\n );\n }\n\n // If an instance already exists and reset is not requested, just return the existing instance\n return SupertabConnect._instance;\n }\n if (reset && SupertabConnect._instance) {\n // ...and if reset is requested and required, clear the existing instance first\n SupertabConnect.resetInstance();\n }\n\n if (!config.apiKey || !config.merchantSystemUrn) {\n throw new Error(\n \"Missing required configuration: apiKey and merchantSystemUrn are required\"\n );\n }\n this.apiKey = config.apiKey;\n this.merchantSystemUrn = config.merchantSystemUrn;\n this.baseUrl = \"https://api-connect.sbx.supertab.co\";\n\n // Register this as the singleton instance\n SupertabConnect._instance = this;\n }\n\n public static resetInstance(): void {\n SupertabConnect._instance = null;\n }\n\n /**\n * Get the JWKS for a given issuer, using cache if available\n * @private\n */\n private async getJwksForIssuer(issuer: string): Promise<any> {\n if (!jwksCache.has(issuer)) {\n const jwksUrl = `${\n this.baseUrl\n }/.well-known/jwks.json/${encodeURIComponent(issuer)}`;\n\n try {\n const jwksResponse = await fetch(jwksUrl);\n if (!jwksResponse.ok) {\n throw new Error(`Failed to fetch JWKS: ${jwksResponse.status}`);\n }\n\n const jwksData = await jwksResponse.json();\n jwksCache.set(issuer, jwksData);\n } catch (error) {\n if (debug) {\n console.error(\"Error fetching JWKS:\", error);\n }\n throw error;\n }\n }\n\n return jwksCache.get(issuer);\n }\n\n /**\n * Verify a JWT token\n * @param token The JWT token to verify\n * @returns A promise that resolves with the verification result\n */\n async verifyToken(token: string): Promise<TokenVerificationResult> {\n // 1. Check if token exists\n if (!token) {\n return {\n valid: false,\n reason: TokenInvalidReason.MISSING_TOKEN,\n };\n }\n\n // 2. Verify header and algorithm\n let header: JWTHeaderParameters;\n try {\n header = decodeProtectedHeader(token) as JWTHeaderParameters;\n } catch (error) {\n if (debug) {\n console.error(\"Invalid JWT header:\", error);\n }\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_HEADER,\n };\n }\n\n if (header.alg !== \"RS256\") {\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_ALG,\n };\n }\n\n // 3. Verify payload and issuer\n let payload: JWTPayload;\n try {\n payload = decodeJwt(token);\n } catch (error) {\n if (debug) {\n console.error(\"Invalid JWT payload:\", error);\n }\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_PAYLOAD,\n };\n }\n\n const issuer: string | undefined = payload.iss;\n if (!issuer || !issuer.startsWith(\"urn:stc:customer:\")) {\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_ISSUER,\n };\n }\n\n // 4. Verify signature\n try {\n const jwks = await this.getJwksForIssuer(issuer);\n\n // Create a key finder function for verification\n const getKey = async (header: JWTHeaderParameters) => {\n const jwk = jwks.keys.find((key: any) => key.kid === header.kid);\n if (!jwk) throw new Error(`No matching key found: ${header.kid}`);\n return jwk;\n };\n\n const result = await jwtVerify(token, getKey, {\n issuer,\n algorithms: [\"RS256\"],\n clockTolerance: \"1m\",\n });\n\n // Success case - token is valid\n return {\n valid: true,\n payload: result.payload,\n };\n } catch (error: any) {\n if (debug) {\n console.error(\"JWT verification failed:\", error);\n }\n\n // Check if token is expired\n if (error.message?.includes(\"exp\")) {\n return {\n valid: false,\n reason: TokenInvalidReason.EXPIRED,\n };\n }\n\n return {\n valid: false,\n reason: TokenInvalidReason.SIGNATURE_VERIFICATION_FAILED,\n };\n }\n }\n\n /**\n * Records an analytics event\n * @param eventName Name of the event to record\n * @param customerToken Optional customer token for the event\n * @param properties Additional properties to include with the event\n * @returns Promise that resolves when the event is recorded\n */\n async recordEvent(\n eventName: string,\n customerToken?: string,\n properties: Record<string, any> = {}\n ): Promise<void> {\n const payload: EventPayload = {\n event_name: eventName,\n customer_system_token: customerToken,\n merchant_system_urn: this.merchantSystemUrn ? this.merchantSystemUrn : \"\",\n properties,\n };\n\n try {\n const response = await fetch(`${this.baseUrl}/events`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n console.log(`Failed to record event: ${response.status}`);\n }\n } catch (error) {\n console.log(\"Error recording event:\", error);\n }\n }\n\n /**\n * Handle the request, report an event to Supertab Connect and return a response\n */\n private async baseHandleRequest(\n token: string,\n url: string,\n user_agent: string,\n ctx: any\n ): Promise<Response> {\n // 1. Verify token\n const verification = await this.verifyToken(token);\n\n // Record event helper\n async function recordEvent(\n stc: SupertabConnect,\n eventName: string,\n ctx: any\n ) {\n const eventProperties = {\n page_url: url,\n user_agent: user_agent,\n verification_status: verification.valid ? \"valid\" : \"invalid\",\n verification_reason: verification.reason || \"success\",\n };\n if (ctx) {\n const eventPromise = stc.recordEvent(eventName, token, eventProperties);\n ctx.waitUntil(eventPromise);\n return eventPromise;\n } else {\n return await stc.recordEvent(eventName, token, eventProperties);\n }\n }\n\n // 2. Handle based on verification result\n if (!verification.valid) {\n await recordEvent(\n this,\n verification.reason || \"token_verification_failed\",\n ctx\n );\n const message =\n \"Payment required: you need to present a valid Supertab Connect token to access this content. \" +\n \"Check out the provided url for details\";\n const details =\n \"❌ Content access denied\" +\n (verification.reason ? `: ${verification.reason}` : \"\");\n const contentAccessUrl = `${this.baseUrl}/merchants/systems/${this.merchantSystemUrn}/content-access.json`;\n\n const responseBody = {\n url: contentAccessUrl,\n message: message,\n details: details,\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 402,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n // 3. Success\n await recordEvent(this, \"page_viewed\", ctx);\n return new Response(\"✅ Content Access granted\", {\n status: 200,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n private extractDataFromRequest(request: Request): {\n token: string;\n url: string;\n user_agent: string;\n } {\n // Parse token\n const auth = request.headers.get(\"Authorization\") || \"\";\n const token = auth.startsWith(\"Bearer \") ? auth.slice(7) : \"\";\n\n // Extract URL and user agent\n const url = request.url;\n const user_agent = request.headers.get(\"User-Agent\") || \"unknown\";\n\n return { token, url, user_agent };\n }\n\n static checkIfBotRequest(request: Request): boolean {\n const userAgent = request.headers.get(\"User-Agent\") || \"\";\n const accept = request.headers.get(\"accept\") || \"\";\n const secChUa = request.headers.get(\"sec-ch-ua\");\n const acceptLanguage = request.headers.get(\"accept-language\");\n const botScore = (request as any).cf?.botManagement?.score;\n\n const botList = [\n \"chatgpt-user\",\n \"perplexitybot\",\n \"gptbot\",\n \"anthropic-ai\",\n \"ccbot\",\n \"claude-web\",\n \"claudebot\",\n \"cohere-ai\",\n \"youbot\",\n \"diffbot\",\n \"oai-searchbot\",\n \"meta-externalagent\",\n \"timpibot\",\n \"amazonbot\",\n \"bytespider\",\n \"perplexity-user\",\n \"googlebot\",\n \"bot\",\n \"curl\",\n \"wget\",\n ];\n // 1. Basic substring check from known list\n const lowerCaseUserAgent = userAgent.toLowerCase();\n const botUaMatch = botList.some((bot) => lowerCaseUserAgent.includes(bot));\n\n // 2. Headless browser detection\n const headlessIndicators =\n userAgent.toLowerCase().includes(\"headless\") ||\n userAgent.toLowerCase().includes(\"puppeteer\") ||\n !secChUa;\n\n const only_sec_ch_ua_missing =\n !userAgent.toLowerCase().includes(\"headless\") ||\n !userAgent.toLowerCase().includes(\"puppeteer\") ||\n !secChUa;\n\n // 3. Suspicious header gaps — many bots omit these\n const missingHeaders = !accept || !acceptLanguage;\n\n // 4. Cloudflare bot score check (if available)\n const lowBotScore = typeof botScore === \"number\" && botScore < 30;\n console.log(\"Bot Detection Details:\", {\n botUaMatch,\n headlessIndicators,\n missingHeaders,\n lowBotScore,\n botScore,\n });\n\n // Safari and Mozilla special case\n if (\n lowerCaseUserAgent.includes(\"safari\") ||\n lowerCaseUserAgent.includes(\"mozilla\")\n ) {\n // Safari is not a bot, but it may be headless\n if (headlessIndicators && only_sec_ch_ua_missing) {\n return false; // Likely not a bot, but missing a Sec-CH-UA header\n }\n }\n\n // Final decision\n return botUaMatch || headlessIndicators || missingHeaders || lowBotScore;\n }\n\n static async cloudflareHandleRequests(\n request: Request,\n env: Env,\n ctx: any\n ): Promise<Response> {\n // Validate required env variables\n const { MERCHANT_SYSTEM_URN, MERCHANT_API_KEY } = env;\n\n // Prepare or get the SupertabConnect instance\n const supertabConnect = new SupertabConnect({\n apiKey: MERCHANT_API_KEY,\n merchantSystemUrn: MERCHANT_SYSTEM_URN,\n });\n\n // Handle the request, including bot detection, token verification and recording the event\n return supertabConnect.handleRequest(\n request,\n SupertabConnect.checkIfBotRequest,\n ctx\n );\n }\n\n static async fastlyHandleRequests(\n request: Request,\n merchantSystemUrn: string,\n merchantApiKey: string\n ): Promise<Response> {\n // Prepare or get the SupertabConnect instance\n const supertabConnect = new SupertabConnect({\n apiKey: merchantApiKey,\n merchantSystemUrn: merchantSystemUrn,\n });\n\n // Handle the request, including bot detection, token verification and recording the event\n return supertabConnect.handleRequest(\n request,\n SupertabConnect.checkIfBotRequest,\n null\n );\n }\n\n async handleRequest(\n request: Request,\n botDetectionHandler?: (request: Request, ctx?: any) => boolean,\n ctx?: any\n ): Promise<Response> {\n // 1. Extract token, URL, and user agent from the request\n const { token, url, user_agent } = this.extractDataFromRequest(request);\n\n // 2. Handle bot detection if provided\n if (botDetectionHandler && !botDetectionHandler(request, ctx)) {\n return new Response(\"✅ Non-Bot Content Access granted\", {\n status: 200,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n // 3. Call the base handle request method and return the result\n return this.baseHandleRequest(token, url, user_agent, ctx);\n }\n\n /** Generate a customer JWT\n * @param customerURN The customer's unique resource name (URN).\n * @param kid The key ID to include in the JWT header.\n * @param privateKeyPem The private key in PEM format used to sign the JWT.\n * @param expirationSeconds The token's expiration time in seconds (default is 3600 seconds).\n * @returns A promise that resolves to the generated JWT as a string.\n */\n static async generateCustomerJWT(\n customerURN: string,\n kid: string,\n privateKeyPem: string,\n expirationSeconds: number = 3600\n ): Promise<string> {\n const alg = \"RS256\";\n const key = await importPKCS8(privateKeyPem, alg);\n\n const now = Math.floor(Date.now() / 1000);\n\n return new SignJWT({})\n .setProtectedHeader({ alg, kid })\n .setIssuer(customerURN)\n .setIssuedAt(now)\n .setExpirationTime(now + expirationSeconds)\n .sign(key);\n }\n}\n"],"mappings":"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","names":["index_exports","__export","SupertabConnect","__toCommonJS","import_jose","jwksCache","debug","_SupertabConnect","config","reset","issuer","jwksUrl","jwksResponse","jwksData","error","token","header","payload","jwks","jwk","key","eventName","customerToken","properties","response","url","user_agent","ctx","verification","recordEvent","stc","eventProperties","eventPromise","message","details","responseBody","request","auth","userAgent","accept","secChUa","acceptLanguage","botScore","botList","lowerCaseUserAgent","botUaMatch","bot","headlessIndicators","only_sec_ch_ua_missing","missingHeaders","lowBotScore","env","MERCHANT_SYSTEM_URN","MERCHANT_API_KEY","merchantSystemUrn","merchantApiKey","botDetectionHandler","customerURN","kid","privateKeyPem","expirationSeconds","alg","now","SupertabConnect"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{jwtVerify as w,decodeProtectedHeader as v,decodeJwt as _,importPKCS8 as R,SignJWT as E}from"jose";var y=new Map,p=!0,a=class a{constructor(e,n=!1){if(!n&&a._instance){if(!(e.apiKey===a._instance.apiKey&&e.merchantSystemUrn===a._instance.merchantSystemUrn))throw new Error("Cannot create a new instance with different configuration. Use resetInstance to clear the existing instance.");return a._instance}if(n&&a._instance&&a.resetInstance(),!e.apiKey||!e.merchantSystemUrn)throw new Error("Missing required configuration: apiKey and merchantSystemUrn are required");this.apiKey=e.apiKey,this.merchantSystemUrn=e.merchantSystemUrn,this.baseUrl="https://api-connect.sbx.supertab.co",a._instance=this}static resetInstance(){a._instance=null}async getJwksForIssuer(e){if(!y.has(e)){let n=`${this.baseUrl}/.well-known/jwks.json/${encodeURIComponent(e)}`;try{let s=await fetch(n);if(!s.ok)throw new Error(`Failed to fetch JWKS: ${s.status}`);let r=await s.json();y.set(e,r)}catch(s){throw p&&console.error("Error fetching JWKS:",s),s}}return y.get(e)}async verifyToken(e){if(!e)return{valid:!1,reason:"missing_token"};let n;try{n=v(e)}catch(t){return p&&console.error("Invalid JWT header:",t),{valid:!1,reason:"invalid_header"}}if(n.alg!=="RS256")return{valid:!1,reason:"invalid_algorithm"};let s;try{s=_(e)}catch(t){return p&&console.error("Invalid JWT payload:",t),{valid:!1,reason:"invalid_payload"}}let r=s.iss;if(!r||!r.startsWith("urn:stc:customer:"))return{valid:!1,reason:"invalid_issuer"};try{let t=await this.getJwksForIssuer(r);return{valid:!0,payload:(await w(e,async i=>{let u=t.keys.find(l=>l.kid===i.kid);if(!u)throw new Error(`No matching key found: ${i.kid}`);return u},{issuer:r,algorithms:["RS256"],clockTolerance:"1m"})).payload}}catch(t){return p&&console.error("JWT verification failed:",t),t.message?.includes("exp")?{valid:!1,reason:"token_expired"}:{valid:!1,reason:"signature_verification_failed"}}}async recordEvent(e,n,s={}){let r={event_name:e,customer_system_token:n,merchant_system_urn:this.merchantSystemUrn?this.merchantSystemUrn:"",properties:s};try{let t=await fetch(`${this.baseUrl}/events`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(r)});t.ok||console.log(`Failed to record event: ${t.status}`)}catch(t){console.log("Error recording event:",t)}}async baseHandleRequest(e,n,s,r){let t=await this.verifyToken(e);async function o(c,i,u){let l={page_url:n,user_agent:s,verification_status:t.valid?"valid":"invalid",verification_reason:t.reason||"success"};if(u){let d=c.recordEvent(i,e,l);return u.waitUntil(d),d}else return await c.recordEvent(i,e,l)}if(!t.valid){await o(this,t.reason||"token_verification_failed",r);let c="Payment required: you need to present a valid Supertab Connect token to access this content. Check out the provided url for details",i="\u274C Content access denied"+(t.reason?`: ${t.reason}`:""),l={url:`${this.baseUrl}/merchants/systems/${this.merchantSystemUrn}/content-access.json`,message:c,details:i};return new Response(JSON.stringify(l),{status:402,headers:new Headers({"Content-Type":"application/json"})})}return await o(this,"page_viewed",r),new Response("\u2705 Content Access granted",{status:200,headers:new Headers({"Content-Type":"application/json"})})}extractDataFromRequest(e){let n=e.headers.get("Authorization")||"",s=n.startsWith("Bearer ")?n.slice(7):"",r=e.url,t=e.headers.get("User-Agent")||"unknown";return{token:s,url:r,user_agent:t}}static checkIfBotRequest(e){let n=e.headers.get("User-Agent")||"",s=e.headers.get("accept")||"",r=e.headers.get("sec-ch-ua"),t=e.headers.get("accept-language"),o=e.cf?.botManagement?.score,c=["chatgpt-user","perplexitybot","gptbot","anthropic-ai","ccbot","claude-web","claudebot","cohere-ai","youbot","diffbot","oai-searchbot","meta-externalagent","timpibot","amazonbot","bytespider","perplexity-user","googlebot","bot","curl","wget"],i=n.toLowerCase(),u=c.some(f=>i.includes(f)),l=n.toLowerCase().includes("headless")||n.toLowerCase().includes("puppeteer")||!r,d=!n.toLowerCase().includes("headless")||!n.toLowerCase().includes("puppeteer")||!r,h=!s||!t,g=typeof o=="number"&&o<30;return console.log("Bot Detection Details:",{botUaMatch:u,headlessIndicators:l,missingHeaders:h,lowBotScore:g,botScore:o}),(i.includes("safari")||i.includes("mozilla"))&&l&&d?!1:u||l||h||g}static async cloudflareHandleRequests(e,n,s){let{MERCHANT_SYSTEM_URN:r,MERCHANT_API_KEY:t}=n;return new a({apiKey:t,merchantSystemUrn:r}).handleRequest(e,a.checkIfBotRequest,s)}static async fastlyHandleRequests(e,n,s){return new a({apiKey:s,merchantSystemUrn:n}).handleRequest(e,a.checkIfBotRequest,null)}async handleRequest(e,n,s){let{token:r,url:t,user_agent:o}=this.extractDataFromRequest(e);return n&&!n(e,s)?new Response("\u2705 Non-Bot Content Access granted",{status:200,headers:new Headers({"Content-Type":"application/json"})}):this.baseHandleRequest(r,t,o,s)}static async generateCustomerJWT(e,n,s,r=3600){let t="RS256",o=await R(s,t),c=Math.floor(Date.now()/1e3);return new E({}).setProtectedHeader({alg:t,kid:n}).setIssuer(e).setIssuedAt(c).setExpirationTime(c+r).sign(o)}};a._instance=null;var m=a;export{m as SupertabConnect};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n SupertabConnectConfig,\n Env,\n EventPayload,\n TokenVerificationResult,\n TokenInvalidReason,\n} from \"./types\";\nimport {\n jwtVerify,\n decodeProtectedHeader,\n decodeJwt,\n JWTHeaderParameters,\n JWTPayload,\n importPKCS8,\n SignJWT,\n} from \"jose\";\n\nexport type { Env } from \"./types\";\n\n// In-memory cache for JWK sets\nconst jwksCache = new Map<string, any>();\nconst debug = true; // Set to true for debugging purposes\n\n/**\n * SupertabConnect class provides higher level methods\n * for using Supertab Connect within supported CDN integrations\n * as well as more specialized methods to customarily verify JWT tokens and record events.\n */\nexport class SupertabConnect {\n private apiKey?: string;\n private baseUrl?: string;\n private merchantSystemUrn?: string;\n\n private static _instance: SupertabConnect | null = null;\n\n public constructor(config: SupertabConnectConfig, reset: boolean = false) {\n if (!reset && SupertabConnect._instance) {\n // If reset was not requested and an instance conflicts with the provided config, throw an error\n if (\n !(\n config.apiKey === SupertabConnect._instance.apiKey &&\n config.merchantSystemUrn ===\n SupertabConnect._instance.merchantSystemUrn\n )\n ) {\n throw new Error(\n \"Cannot create a new instance with different configuration. Use resetInstance to clear the existing instance.\"\n );\n }\n\n // If an instance already exists and reset is not requested, just return the existing instance\n return SupertabConnect._instance;\n }\n if (reset && SupertabConnect._instance) {\n // ...and if reset is requested and required, clear the existing instance first\n SupertabConnect.resetInstance();\n }\n\n if (!config.apiKey || !config.merchantSystemUrn) {\n throw new Error(\n \"Missing required configuration: apiKey and merchantSystemUrn are required\"\n );\n }\n this.apiKey = config.apiKey;\n this.merchantSystemUrn = config.merchantSystemUrn;\n this.baseUrl = \"https://api-connect.sbx.supertab.co\";\n\n // Register this as the singleton instance\n SupertabConnect._instance = this;\n }\n\n public static resetInstance(): void {\n SupertabConnect._instance = null;\n }\n\n /**\n * Get the JWKS for a given issuer, using cache if available\n * @private\n */\n private async getJwksForIssuer(issuer: string): Promise<any> {\n if (!jwksCache.has(issuer)) {\n const jwksUrl = `${\n this.baseUrl\n }/.well-known/jwks.json/${encodeURIComponent(issuer)}`;\n\n try {\n const jwksResponse = await fetch(jwksUrl);\n if (!jwksResponse.ok) {\n throw new Error(`Failed to fetch JWKS: ${jwksResponse.status}`);\n }\n\n const jwksData = await jwksResponse.json();\n jwksCache.set(issuer, jwksData);\n } catch (error) {\n if (debug) {\n console.error(\"Error fetching JWKS:\", error);\n }\n throw error;\n }\n }\n\n return jwksCache.get(issuer);\n }\n\n /**\n * Verify a JWT token\n * @param token The JWT token to verify\n * @returns A promise that resolves with the verification result\n */\n async verifyToken(token: string): Promise<TokenVerificationResult> {\n // 1. Check if token exists\n if (!token) {\n return {\n valid: false,\n reason: TokenInvalidReason.MISSING_TOKEN,\n };\n }\n\n // 2. Verify header and algorithm\n let header: JWTHeaderParameters;\n try {\n header = decodeProtectedHeader(token) as JWTHeaderParameters;\n } catch (error) {\n if (debug) {\n console.error(\"Invalid JWT header:\", error);\n }\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_HEADER,\n };\n }\n\n if (header.alg !== \"RS256\") {\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_ALG,\n };\n }\n\n // 3. Verify payload and issuer\n let payload: JWTPayload;\n try {\n payload = decodeJwt(token);\n } catch (error) {\n if (debug) {\n console.error(\"Invalid JWT payload:\", error);\n }\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_PAYLOAD,\n };\n }\n\n const issuer: string | undefined = payload.iss;\n if (!issuer || !issuer.startsWith(\"urn:stc:customer:\")) {\n return {\n valid: false,\n reason: TokenInvalidReason.INVALID_ISSUER,\n };\n }\n\n // 4. Verify signature\n try {\n const jwks = await this.getJwksForIssuer(issuer);\n\n // Create a key finder function for verification\n const getKey = async (header: JWTHeaderParameters) => {\n const jwk = jwks.keys.find((key: any) => key.kid === header.kid);\n if (!jwk) throw new Error(`No matching key found: ${header.kid}`);\n return jwk;\n };\n\n const result = await jwtVerify(token, getKey, {\n issuer,\n algorithms: [\"RS256\"],\n clockTolerance: \"1m\",\n });\n\n // Success case - token is valid\n return {\n valid: true,\n payload: result.payload,\n };\n } catch (error: any) {\n if (debug) {\n console.error(\"JWT verification failed:\", error);\n }\n\n // Check if token is expired\n if (error.message?.includes(\"exp\")) {\n return {\n valid: false,\n reason: TokenInvalidReason.EXPIRED,\n };\n }\n\n return {\n valid: false,\n reason: TokenInvalidReason.SIGNATURE_VERIFICATION_FAILED,\n };\n }\n }\n\n /**\n * Records an analytics event\n * @param eventName Name of the event to record\n * @param customerToken Optional customer token for the event\n * @param properties Additional properties to include with the event\n * @returns Promise that resolves when the event is recorded\n */\n async recordEvent(\n eventName: string,\n customerToken?: string,\n properties: Record<string, any> = {}\n ): Promise<void> {\n const payload: EventPayload = {\n event_name: eventName,\n customer_system_token: customerToken,\n merchant_system_urn: this.merchantSystemUrn ? this.merchantSystemUrn : \"\",\n properties,\n };\n\n try {\n const response = await fetch(`${this.baseUrl}/events`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n console.log(`Failed to record event: ${response.status}`);\n }\n } catch (error) {\n console.log(\"Error recording event:\", error);\n }\n }\n\n /**\n * Handle the request, report an event to Supertab Connect and return a response\n */\n private async baseHandleRequest(\n token: string,\n url: string,\n user_agent: string,\n ctx: any\n ): Promise<Response> {\n // 1. Verify token\n const verification = await this.verifyToken(token);\n\n // Record event helper\n async function recordEvent(\n stc: SupertabConnect,\n eventName: string,\n ctx: any\n ) {\n const eventProperties = {\n page_url: url,\n user_agent: user_agent,\n verification_status: verification.valid ? \"valid\" : \"invalid\",\n verification_reason: verification.reason || \"success\",\n };\n if (ctx) {\n const eventPromise = stc.recordEvent(eventName, token, eventProperties);\n ctx.waitUntil(eventPromise);\n return eventPromise;\n } else {\n return await stc.recordEvent(eventName, token, eventProperties);\n }\n }\n\n // 2. Handle based on verification result\n if (!verification.valid) {\n await recordEvent(\n this,\n verification.reason || \"token_verification_failed\",\n ctx\n );\n const message =\n \"Payment required: you need to present a valid Supertab Connect token to access this content. \" +\n \"Check out the provided url for details\";\n const details =\n \"❌ Content access denied\" +\n (verification.reason ? `: ${verification.reason}` : \"\");\n const contentAccessUrl = `${this.baseUrl}/merchants/systems/${this.merchantSystemUrn}/content-access.json`;\n\n const responseBody = {\n url: contentAccessUrl,\n message: message,\n details: details,\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 402,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n // 3. Success\n await recordEvent(this, \"page_viewed\", ctx);\n return new Response(\"✅ Content Access granted\", {\n status: 200,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n private extractDataFromRequest(request: Request): {\n token: string;\n url: string;\n user_agent: string;\n } {\n // Parse token\n const auth = request.headers.get(\"Authorization\") || \"\";\n const token = auth.startsWith(\"Bearer \") ? auth.slice(7) : \"\";\n\n // Extract URL and user agent\n const url = request.url;\n const user_agent = request.headers.get(\"User-Agent\") || \"unknown\";\n\n return { token, url, user_agent };\n }\n\n static checkIfBotRequest(request: Request): boolean {\n const userAgent = request.headers.get(\"User-Agent\") || \"\";\n const accept = request.headers.get(\"accept\") || \"\";\n const secChUa = request.headers.get(\"sec-ch-ua\");\n const acceptLanguage = request.headers.get(\"accept-language\");\n const botScore = (request as any).cf?.botManagement?.score;\n\n const botList = [\n \"chatgpt-user\",\n \"perplexitybot\",\n \"gptbot\",\n \"anthropic-ai\",\n \"ccbot\",\n \"claude-web\",\n \"claudebot\",\n \"cohere-ai\",\n \"youbot\",\n \"diffbot\",\n \"oai-searchbot\",\n \"meta-externalagent\",\n \"timpibot\",\n \"amazonbot\",\n \"bytespider\",\n \"perplexity-user\",\n \"googlebot\",\n \"bot\",\n \"curl\",\n \"wget\",\n ];\n // 1. Basic substring check from known list\n const lowerCaseUserAgent = userAgent.toLowerCase();\n const botUaMatch = botList.some((bot) => lowerCaseUserAgent.includes(bot));\n\n // 2. Headless browser detection\n const headlessIndicators =\n userAgent.toLowerCase().includes(\"headless\") ||\n userAgent.toLowerCase().includes(\"puppeteer\") ||\n !secChUa;\n\n const only_sec_ch_ua_missing =\n !userAgent.toLowerCase().includes(\"headless\") ||\n !userAgent.toLowerCase().includes(\"puppeteer\") ||\n !secChUa;\n\n // 3. Suspicious header gaps — many bots omit these\n const missingHeaders = !accept || !acceptLanguage;\n\n // 4. Cloudflare bot score check (if available)\n const lowBotScore = typeof botScore === \"number\" && botScore < 30;\n console.log(\"Bot Detection Details:\", {\n botUaMatch,\n headlessIndicators,\n missingHeaders,\n lowBotScore,\n botScore,\n });\n\n // Safari and Mozilla special case\n if (\n lowerCaseUserAgent.includes(\"safari\") ||\n lowerCaseUserAgent.includes(\"mozilla\")\n ) {\n // Safari is not a bot, but it may be headless\n if (headlessIndicators && only_sec_ch_ua_missing) {\n return false; // Likely not a bot, but missing a Sec-CH-UA header\n }\n }\n\n // Final decision\n return botUaMatch || headlessIndicators || missingHeaders || lowBotScore;\n }\n\n static async cloudflareHandleRequests(\n request: Request,\n env: Env,\n ctx: any\n ): Promise<Response> {\n // Validate required env variables\n const { MERCHANT_SYSTEM_URN, MERCHANT_API_KEY } = env;\n\n // Prepare or get the SupertabConnect instance\n const supertabConnect = new SupertabConnect({\n apiKey: MERCHANT_API_KEY,\n merchantSystemUrn: MERCHANT_SYSTEM_URN,\n });\n\n // Handle the request, including bot detection, token verification and recording the event\n return supertabConnect.handleRequest(\n request,\n SupertabConnect.checkIfBotRequest,\n ctx\n );\n }\n\n static async fastlyHandleRequests(\n request: Request,\n merchantSystemUrn: string,\n merchantApiKey: string\n ): Promise<Response> {\n // Prepare or get the SupertabConnect instance\n const supertabConnect = new SupertabConnect({\n apiKey: merchantApiKey,\n merchantSystemUrn: merchantSystemUrn,\n });\n\n // Handle the request, including bot detection, token verification and recording the event\n return supertabConnect.handleRequest(\n request,\n SupertabConnect.checkIfBotRequest,\n null\n );\n }\n\n async handleRequest(\n request: Request,\n botDetectionHandler?: (request: Request, ctx?: any) => boolean,\n ctx?: any\n ): Promise<Response> {\n // 1. Extract token, URL, and user agent from the request\n const { token, url, user_agent } = this.extractDataFromRequest(request);\n\n // 2. Handle bot detection if provided\n if (botDetectionHandler && !botDetectionHandler(request, ctx)) {\n return new Response(\"✅ Non-Bot Content Access granted\", {\n status: 200,\n headers: new Headers({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n // 3. Call the base handle request method and return the result\n return this.baseHandleRequest(token, url, user_agent, ctx);\n }\n\n /** Generate a customer JWT\n * @param customerURN The customer's unique resource name (URN).\n * @param kid The key ID to include in the JWT header.\n * @param privateKeyPem The private key in PEM format used to sign the JWT.\n * @param expirationSeconds The token's expiration time in seconds (default is 3600 seconds).\n * @returns A promise that resolves to the generated JWT as a string.\n */\n static async generateCustomerJWT(\n customerURN: string,\n kid: string,\n privateKeyPem: string,\n expirationSeconds: number = 3600\n ): Promise<string> {\n const alg = \"RS256\";\n const key = await importPKCS8(privateKeyPem, alg);\n\n const now = Math.floor(Date.now() / 1000);\n\n return new SignJWT({})\n .setProtectedHeader({ alg, kid })\n .setIssuer(customerURN)\n .setIssuedAt(now)\n .setExpirationTime(now + expirationSeconds)\n .sign(key);\n }\n}\n"],"mappings":"AAOA,OACE,aAAAA,EACA,yBAAAC,EACA,aAAAC,EAGA,eAAAC,EACA,WAAAC,MACK,OAKP,IAAMC,EAAY,IAAI,IAChBC,EAAQ,GAODC,EAAN,MAAMA,CAAgB,CAOpB,YAAYC,EAA+BC,EAAiB,GAAO,CACxE,GAAI,CAACA,GAASF,EAAgB,UAAW,CAEvC,GACE,EACEC,EAAO,SAAWD,EAAgB,UAAU,QAC5CC,EAAO,oBACLD,EAAgB,UAAU,mBAG9B,MAAM,IAAI,MACR,8GACF,EAIF,OAAOA,EAAgB,SACzB,CAMA,GALIE,GAASF,EAAgB,WAE3BA,EAAgB,cAAc,EAG5B,CAACC,EAAO,QAAU,CAACA,EAAO,kBAC5B,MAAM,IAAI,MACR,2EACF,EAEF,KAAK,OAASA,EAAO,OACrB,KAAK,kBAAoBA,EAAO,kBAChC,KAAK,QAAU,sCAGfD,EAAgB,UAAY,IAC9B,CAEA,OAAc,eAAsB,CAClCA,EAAgB,UAAY,IAC9B,CAMA,MAAc,iBAAiBG,EAA8B,CAC3D,GAAI,CAACL,EAAU,IAAIK,CAAM,EAAG,CAC1B,IAAMC,EAAU,GACd,KAAK,OACP,0BAA0B,mBAAmBD,CAAM,CAAC,GAEpD,GAAI,CACF,IAAME,EAAe,MAAM,MAAMD,CAAO,EACxC,GAAI,CAACC,EAAa,GAChB,MAAM,IAAI,MAAM,yBAAyBA,EAAa,MAAM,EAAE,EAGhE,IAAMC,EAAW,MAAMD,EAAa,KAAK,EACzCP,EAAU,IAAIK,EAAQG,CAAQ,CAChC,OAASC,EAAO,CACd,MAAIR,GACF,QAAQ,MAAM,uBAAwBQ,CAAK,EAEvCA,CACR,CACF,CAEA,OAAOT,EAAU,IAAIK,CAAM,CAC7B,CAOA,MAAM,YAAYK,EAAiD,CAEjE,GAAI,CAACA,EACH,MAAO,CACL,MAAO,GACP,sBACF,EAIF,IAAIC,EACJ,GAAI,CACFA,EAASf,EAAsBc,CAAK,CACtC,OAASD,EAAO,CACd,OAAIR,GACF,QAAQ,MAAM,sBAAuBQ,CAAK,EAErC,CACL,MAAO,GACP,uBACF,CACF,CAEA,GAAIE,EAAO,MAAQ,QACjB,MAAO,CACL,MAAO,GACP,0BACF,EAIF,IAAIC,EACJ,GAAI,CACFA,EAAUf,EAAUa,CAAK,CAC3B,OAASD,EAAO,CACd,OAAIR,GACF,QAAQ,MAAM,uBAAwBQ,CAAK,EAEtC,CACL,MAAO,GACP,wBACF,CACF,CAEA,IAAMJ,EAA6BO,EAAQ,IAC3C,GAAI,CAACP,GAAU,CAACA,EAAO,WAAW,mBAAmB,EACnD,MAAO,CACL,MAAO,GACP,uBACF,EAIF,GAAI,CACF,IAAMQ,EAAO,MAAM,KAAK,iBAAiBR,CAAM,EAgB/C,MAAO,CACL,MAAO,GACP,SATa,MAAMV,EAAUe,EANhB,MAAOC,GAAgC,CACpD,IAAMG,EAAMD,EAAK,KAAK,KAAME,GAAaA,EAAI,MAAQJ,EAAO,GAAG,EAC/D,GAAI,CAACG,EAAK,MAAM,IAAI,MAAM,0BAA0BH,EAAO,GAAG,EAAE,EAChE,OAAOG,CACT,EAE8C,CAC5C,OAAAT,EACA,WAAY,CAAC,OAAO,EACpB,eAAgB,IAClB,CAAC,GAKiB,OAClB,CACF,OAASI,EAAY,CAMnB,OALIR,GACF,QAAQ,MAAM,2BAA4BQ,CAAK,EAI7CA,EAAM,SAAS,SAAS,KAAK,EACxB,CACL,MAAO,GACP,sBACF,EAGK,CACL,MAAO,GACP,sCACF,CACF,CACF,CASA,MAAM,YACJO,EACAC,EACAC,EAAkC,CAAC,EACpB,CACf,IAAMN,EAAwB,CAC5B,WAAYI,EACZ,sBAAuBC,EACvB,oBAAqB,KAAK,kBAAoB,KAAK,kBAAoB,GACvE,WAAAC,CACF,EAEA,GAAI,CACF,IAAMC,EAAW,MAAM,MAAM,GAAG,KAAK,OAAO,UAAW,CACrD,OAAQ,OACR,QAAS,CACP,cAAe,UAAU,KAAK,MAAM,GACpC,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAUP,CAAO,CAC9B,CAAC,EAEIO,EAAS,IACZ,QAAQ,IAAI,2BAA2BA,EAAS,MAAM,EAAE,CAE5D,OAASV,EAAO,CACd,QAAQ,IAAI,yBAA0BA,CAAK,CAC7C,CACF,CAKA,MAAc,kBACZC,EACAU,EACAC,EACAC,EACmB,CAEnB,IAAMC,EAAe,MAAM,KAAK,YAAYb,CAAK,EAGjD,eAAec,EACbC,EACAT,EACAM,EACA,CACA,IAAMI,EAAkB,CACtB,SAAUN,EACV,WAAYC,EACZ,oBAAqBE,EAAa,MAAQ,QAAU,UACpD,oBAAqBA,EAAa,QAAU,SAC9C,EACA,GAAID,EAAK,CACP,IAAMK,EAAeF,EAAI,YAAYT,EAAWN,EAAOgB,CAAe,EACtE,OAAAJ,EAAI,UAAUK,CAAY,EACnBA,CACT,KACE,QAAO,MAAMF,EAAI,YAAYT,EAAWN,EAAOgB,CAAe,CAElE,CAGA,GAAI,CAACH,EAAa,MAAO,CACvB,MAAMC,EACJ,KACAD,EAAa,QAAU,4BACvBD,CACF,EACA,IAAMM,EACJ,sIAEIC,EACJ,gCACCN,EAAa,OAAS,KAAKA,EAAa,MAAM,GAAK,IAGhDO,EAAe,CACnB,IAHuB,GAAG,KAAK,OAAO,sBAAsB,KAAK,iBAAiB,uBAIlF,QAASF,EACT,QAASC,CACX,EAEA,OAAO,IAAI,SAAS,KAAK,UAAUC,CAAY,EAAG,CAChD,OAAQ,IACR,QAAS,IAAI,QAAQ,CAAE,eAAgB,kBAAmB,CAAC,CAC7D,CAAC,CACH,CAGA,aAAMN,EAAY,KAAM,cAAeF,CAAG,EACnC,IAAI,SAAS,gCAA4B,CAC9C,OAAQ,IACR,QAAS,IAAI,QAAQ,CAAE,eAAgB,kBAAmB,CAAC,CAC7D,CAAC,CACH,CAEQ,uBAAuBS,EAI7B,CAEA,IAAMC,EAAOD,EAAQ,QAAQ,IAAI,eAAe,GAAK,GAC/CrB,EAAQsB,EAAK,WAAW,SAAS,EAAIA,EAAK,MAAM,CAAC,EAAI,GAGrDZ,EAAMW,EAAQ,IACdV,EAAaU,EAAQ,QAAQ,IAAI,YAAY,GAAK,UAExD,MAAO,CAAE,MAAArB,EAAO,IAAAU,EAAK,WAAAC,CAAW,CAClC,CAEA,OAAO,kBAAkBU,EAA2B,CAClD,IAAME,EAAYF,EAAQ,QAAQ,IAAI,YAAY,GAAK,GACjDG,EAASH,EAAQ,QAAQ,IAAI,QAAQ,GAAK,GAC1CI,EAAUJ,EAAQ,QAAQ,IAAI,WAAW,EACzCK,EAAiBL,EAAQ,QAAQ,IAAI,iBAAiB,EACtDM,EAAYN,EAAgB,IAAI,eAAe,MAE/CO,EAAU,CACd,eACA,gBACA,SACA,eACA,QACA,aACA,YACA,YACA,SACA,UACA,gBACA,qBACA,WACA,YACA,aACA,kBACA,YACA,MACA,OACA,MACF,EAEMC,EAAqBN,EAAU,YAAY,EAC3CO,EAAaF,EAAQ,KAAMG,GAAQF,EAAmB,SAASE,CAAG,CAAC,EAGnEC,EACJT,EAAU,YAAY,EAAE,SAAS,UAAU,GAC3CA,EAAU,YAAY,EAAE,SAAS,WAAW,GAC5C,CAACE,EAEGQ,EACJ,CAACV,EAAU,YAAY,EAAE,SAAS,UAAU,GAC5C,CAACA,EAAU,YAAY,EAAE,SAAS,WAAW,GAC7C,CAACE,EAGGS,EAAiB,CAACV,GAAU,CAACE,EAG7BS,EAAc,OAAOR,GAAa,UAAYA,EAAW,GAU/D,OATA,QAAQ,IAAI,yBAA0B,CACpC,WAAAG,EACA,mBAAAE,EACA,eAAAE,EACA,YAAAC,EACA,SAAAR,CACF,CAAC,GAICE,EAAmB,SAAS,QAAQ,GACpCA,EAAmB,SAAS,SAAS,IAGjCG,GAAsBC,EACjB,GAKJH,GAAcE,GAAsBE,GAAkBC,CAC/D,CAEA,aAAa,yBACXd,EACAe,EACAxB,EACmB,CAEnB,GAAM,CAAE,oBAAAyB,EAAqB,iBAAAC,CAAiB,EAAIF,EASlD,OANwB,IAAI5C,EAAgB,CAC1C,OAAQ8C,EACR,kBAAmBD,CACrB,CAAC,EAGsB,cACrBhB,EACA7B,EAAgB,kBAChBoB,CACF,CACF,CAEA,aAAa,qBACXS,EACAkB,EACAC,EACmB,CAQnB,OANwB,IAAIhD,EAAgB,CAC1C,OAAQgD,EACR,kBAAmBD,CACrB,CAAC,EAGsB,cACrBlB,EACA7B,EAAgB,kBAChB,IACF,CACF,CAEA,MAAM,cACJ6B,EACAoB,EACA7B,EACmB,CAEnB,GAAM,CAAE,MAAAZ,EAAO,IAAAU,EAAK,WAAAC,CAAW,EAAI,KAAK,uBAAuBU,CAAO,EAGtE,OAAIoB,GAAuB,CAACA,EAAoBpB,EAAST,CAAG,EACnD,IAAI,SAAS,wCAAoC,CACtD,OAAQ,IACR,QAAS,IAAI,QAAQ,CAAE,eAAgB,kBAAmB,CAAC,CAC7D,CAAC,EAII,KAAK,kBAAkBZ,EAAOU,EAAKC,EAAYC,CAAG,CAC3D,CASA,aAAa,oBACX8B,EACAC,EACAC,EACAC,EAA4B,KACX,CACjB,IAAMC,EAAM,QACNzC,EAAM,MAAMjB,EAAYwD,EAAeE,CAAG,EAE1CC,EAAM,KAAK,MAAM,KAAK,IAAI,EAAI,GAAI,EAExC,OAAO,IAAI1D,EAAQ,CAAC,CAAC,EAClB,mBAAmB,CAAE,IAAAyD,EAAK,IAAAH,CAAI,CAAC,EAC/B,UAAUD,CAAW,EACrB,YAAYK,CAAG,EACf,kBAAkBA,EAAMF,CAAiB,EACzC,KAAKxC,CAAG,CACb,CACF,EAtcab,EAKI,UAAoC,KAL9C,IAAMwD,EAANxD","names":["jwtVerify","decodeProtectedHeader","decodeJwt","importPKCS8","SignJWT","jwksCache","debug","_SupertabConnect","config","reset","issuer","jwksUrl","jwksResponse","jwksData","error","token","header","payload","jwks","jwk","key","eventName","customerToken","properties","response","url","user_agent","ctx","verification","recordEvent","stc","eventProperties","eventPromise","message","details","responseBody","request","auth","userAgent","accept","secChUa","acceptLanguage","botScore","botList","lowerCaseUserAgent","botUaMatch","bot","headlessIndicators","only_sec_ch_ua_missing","missingHeaders","lowBotScore","env","MERCHANT_SYSTEM_URN","MERCHANT_API_KEY","merchantSystemUrn","merchantApiKey","botDetectionHandler","customerURN","kid","privateKeyPem","expirationSeconds","alg","now","SupertabConnect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getsupertab/supertab-connect-sdk",
|
|
3
|
+
"version": "0.1.0-beta.18",
|
|
4
|
+
"description": "Supertab Connect SDK (beta)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"sideEffects": true,
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"supertab",
|
|
24
|
+
"events"
|
|
25
|
+
],
|
|
26
|
+
"author": "Supertab (https://supertab.co)",
|
|
27
|
+
"homepage": "https://supertab-connect.mintlify.app/introduction/about-supertab-connect",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^18.0.0",
|
|
31
|
+
"tsup": "^8.5.0",
|
|
32
|
+
"typescript": "^5.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"jose": "^6.0.11"
|
|
36
|
+
}
|
|
37
|
+
}
|