@safepassage/sdk 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SafePassage
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,222 @@
1
+ # SafePassage SDK v3.0.0 - Redirect Implementation
2
+
3
+ A lightweight SDK for integrating SafePassage age verification using a simple redirect flow.
4
+
5
+ ## Features
6
+
7
+ - **Ultra-lightweight**: Only 4.9KB minified (1.9KB gzipped)
8
+ - **Simple integration**: Just 10 lines of code
9
+ - **Two modes**: Same-tab redirect or new-tab popup
10
+ - **TypeScript support**: Full type definitions included
11
+ - **Auto-environment detection**: Works seamlessly in development
12
+ - **Secure**: Merchant-generated session IDs prevent attacks
13
+ - **Compliant**: Enforces minimum age of 25
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @safepassage/sdk
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```javascript
24
+ // Initialize SDK
25
+ const sp = new SafePassage({
26
+ apiKey: 'pk_live_xxxxx',
27
+ returnUrl: 'https://yoursite.com/verified',
28
+ cancelUrl: 'https://yoursite.com/cancelled'
29
+ });
30
+
31
+ // Trigger verification
32
+ sp.verify({
33
+ sessionId: generateUUID() // You must generate this
34
+ });
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ | Option | Type | Required | Description |
40
+ |--------|------|----------|-------------|
41
+ | apiKey | string | Yes | Your public API key (pk_live_xxx or pk_test_xxx) |
42
+ | returnUrl | string | Yes | URL to redirect after successful verification |
43
+ | cancelUrl | string | Yes | URL to redirect if user cancels |
44
+ | environment | string | No | 'production', 'staging', or 'development' (auto-detected) |
45
+ | mode | string | No | 'redirect' (default) or 'new-tab' |
46
+ | onComplete | function | No | Callback for new-tab mode completion |
47
+ | onCancel | function | No | Callback for new-tab mode cancellation |
48
+ | onError | function | No | Error handler |
49
+
50
+ ## Verification Options
51
+
52
+ ```javascript
53
+ sp.verify({
54
+ sessionId: 'uuid-v4', // Required: merchant-generated UUID
55
+ challengeAge: 30, // Optional: min 25 (default from dashboard)
56
+ verificationMode: 'L2' // Optional: 'L1' or 'L2' (default from dashboard)
57
+ });
58
+ ```
59
+
60
+ ## Modes
61
+
62
+ ### Same-Tab Redirect (Default)
63
+ User is redirected to SafePassage, then back to your site:
64
+
65
+ ```javascript
66
+ const sp = new SafePassage({
67
+ apiKey: 'pk_live_xxxxx',
68
+ returnUrl: '/age-verified',
69
+ cancelUrl: '/age-gate'
70
+ });
71
+
72
+ sp.verify({ sessionId: generateUUID() });
73
+ ```
74
+
75
+ ### New-Tab Mode
76
+ Verification opens in a popup window:
77
+
78
+ ```javascript
79
+ const sp = new SafePassage({
80
+ apiKey: 'pk_live_xxxxx',
81
+ returnUrl: '/age-verified',
82
+ cancelUrl: '/age-gate',
83
+ mode: 'new-tab',
84
+ onComplete: (result) => {
85
+ console.log('Verified:', result.sessionId);
86
+ // Validate on your server!
87
+ },
88
+ onCancel: () => {
89
+ console.log('User cancelled');
90
+ }
91
+ });
92
+
93
+ sp.verify({ sessionId: generateUUID() });
94
+ ```
95
+
96
+ ## Server-Side Validation (Required!)
97
+
98
+ Always validate the session on your server:
99
+
100
+ ```javascript
101
+ // Node.js example
102
+ const response = await fetch('https://api.safepassageapp.com/v1/sessions/validate', {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Authorization': 'Bearer sk_live_xxxxx', // Secret key
106
+ 'Content-Type': 'application/json'
107
+ },
108
+ body: JSON.stringify({ sessionId })
109
+ });
110
+
111
+ const result = await response.json();
112
+ if (result.verified && result.estimatedAge >= result.challengeAge) {
113
+ // Grant access
114
+ }
115
+ ```
116
+
117
+ ## UUID Generation
118
+
119
+ You must generate session IDs on your end:
120
+
121
+ ```javascript
122
+ function generateUUID() {
123
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
124
+ const r = Math.random() * 16 | 0;
125
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
126
+ return v.toString(16);
127
+ });
128
+ }
129
+ ```
130
+
131
+ ## Complete Example
132
+
133
+ ```html
134
+ <!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <!-- Install via NPM: npm install @safepassage/sdk -->
138
+ <script src="node_modules/@safepassage/sdk/dist/safepassage.min.js"></script>
139
+ </head>
140
+ <body>
141
+ <button onclick="verifyAge()">Verify Your Age</button>
142
+
143
+ <script>
144
+ function generateUUID() {
145
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
146
+ const r = Math.random() * 16 | 0;
147
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
148
+ return v.toString(16);
149
+ });
150
+ }
151
+
152
+ const sp = new SafePassage({
153
+ apiKey: 'pk_live_xxxxx',
154
+ returnUrl: window.location.href + '?verified=true',
155
+ cancelUrl: window.location.href
156
+ });
157
+
158
+ function verifyAge() {
159
+ const sessionId = generateUUID();
160
+ sessionStorage.setItem('pendingVerification', sessionId);
161
+ sp.verify({ sessionId });
162
+ }
163
+
164
+ // Check if returning from verification
165
+ const urlParams = new URLSearchParams(window.location.search);
166
+ if (urlParams.get('verified') === 'true') {
167
+ const sessionId = sessionStorage.getItem('pendingVerification');
168
+ // Validate session server-side here
169
+ console.log('Validate session:', sessionId);
170
+ }
171
+ </script>
172
+ </body>
173
+ </html>
174
+ ```
175
+
176
+ ## TypeScript
177
+
178
+ Full TypeScript support included:
179
+
180
+ ```typescript
181
+ import { SafePassage, SafePassageConfig } from '@safepassage/sdk';
182
+
183
+ const config: SafePassageConfig = {
184
+ apiKey: process.env.SAFEPASSAGE_PUBLIC_KEY!,
185
+ returnUrl: '/verified',
186
+ cancelUrl: '/cancelled'
187
+ };
188
+
189
+ const sp = new SafePassage(config);
190
+ ```
191
+
192
+ ## Migration from v2 (iframe)
193
+
194
+ Old iframe approach (1000+ lines):
195
+ ```javascript
196
+ // Complex iframe setup...
197
+ ```
198
+
199
+ New redirect approach (10 lines):
200
+ ```javascript
201
+ const sp = new SafePassage({
202
+ apiKey: 'pk_live_xxxxx',
203
+ returnUrl: '/verified',
204
+ cancelUrl: '/cancelled'
205
+ });
206
+ sp.verify({ sessionId: generateUUID() });
207
+ ```
208
+
209
+ ## Browser Support
210
+
211
+ - Chrome 60+
212
+ - Firefox 60+
213
+ - Safari 12+
214
+ - Edge 79+
215
+ - Mobile browsers
216
+
217
+ ## Security Notes
218
+
219
+ 1. Always generate session IDs on the merchant side
220
+ 2. Validate sessions server-side before granting access
221
+ 3. Pre-register callback URLs in your dashboard
222
+ 4. Never expose your secret key (sk_live_xxx)
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { SafePassageOptions } from '../types';
3
+ declare const SafePassageVerification: React.FC<SafePassageOptions>;
4
+ export default SafePassageVerification;
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useSafePassage } from '../hooks/useSafePassage';
3
+ const SafePassageVerification = ({ wsUrl, mode = 'enhanced_verification', maxReconnectAttempts = 5, reconnectDelay = 3000, frameRate = 10, imageQuality = 0.8, config_overrides, onSuccess, onFailure, onStateChange, customMessages, className = '', style = {} }) => {
4
+ const { error, feedback, processingStep, zoneTransition, qualityScore, videoRef, canvasRef, isSessionStarted } = useSafePassage({
5
+ wsUrl,
6
+ mode,
7
+ maxReconnectAttempts,
8
+ reconnectDelay,
9
+ frameRate,
10
+ imageQuality,
11
+ config_overrides
12
+ }, {
13
+ onSuccess,
14
+ onFailure,
15
+ onStateChange
16
+ }, customMessages);
17
+ const getDisplayMessage = () => {
18
+ if (zoneTransition) {
19
+ return zoneTransition.message;
20
+ }
21
+ if (feedback && !processingStep) {
22
+ return feedback;
23
+ }
24
+ if (processingStep) {
25
+ return processingStep;
26
+ }
27
+ if (!isSessionStarted) {
28
+ return 'Starting camera...';
29
+ }
30
+ return 'Position your face in the camera and look forward';
31
+ };
32
+ const getMessageClass = () => {
33
+ if (zoneTransition)
34
+ return 'safepassage-instruction-zone-transition';
35
+ if (feedback && !processingStep)
36
+ return 'safepassage-instruction-feedback';
37
+ if (processingStep)
38
+ return 'safepassage-instruction-processing';
39
+ return 'safepassage-instruction-default';
40
+ };
41
+ // Calculate glow intensity based on quality score and feedback
42
+ const getGlowIntensity = () => {
43
+ // Base glow on quality score (0-1)
44
+ let intensity = qualityScore;
45
+ // Boost intensity for positive feedback (both generic and zone-specific)
46
+ if (feedback === 'good_position' ||
47
+ feedback === 'good_wide_position' ||
48
+ feedback === 'good_close_position') {
49
+ intensity = Math.max(intensity, 0.9);
50
+ }
51
+ // Medium intensity for zone positioning messages
52
+ if (feedback === 'position_at_comfortable_distance' ||
53
+ feedback === 'move_closer_to_camera') {
54
+ intensity = Math.max(intensity, 0.6);
55
+ }
56
+ // Reduce intensity for negative feedback
57
+ if (feedback === 'no_face_detected' ||
58
+ feedback === 'move_closer' ||
59
+ feedback === 'move_further' ||
60
+ feedback === 'center_face' ||
61
+ feedback === 'improve_lighting') {
62
+ intensity = Math.min(intensity, 0.3);
63
+ }
64
+ // Special handling for zone transitions - pulse effect
65
+ if (zoneTransition) {
66
+ intensity = Math.max(intensity, 0.7);
67
+ }
68
+ // Clamp between 0 and 1
69
+ return Math.max(0, Math.min(1, intensity));
70
+ };
71
+ const glowIntensity = getGlowIntensity();
72
+ return (_jsxs("div", { className: `safepassage-verification-container ${className}`, style: style, children: [_jsxs("div", { className: "safepassage-video-container", children: [_jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, className: "safepassage-verification-video" }), _jsx("div", { className: "safepassage-video-overlay", children: _jsx("div", { className: "safepassage-oval-frame", style: {
73
+ '--glow-intensity': glowIntensity,
74
+ '--glow-opacity': glowIntensity > 0.1 ? glowIntensity : 0
75
+ } }) })] }), _jsx("div", { className: "safepassage-instructions-area", children: _jsx("div", { className: `safepassage-instruction-text ${getMessageClass()}`, children: getDisplayMessage() }) }), _jsx("canvas", { ref: canvasRef, style: { display: 'none' } }), error && (_jsx("div", { className: "safepassage-error-message", children: error })), _jsx("style", { children: `
76
+ .safepassage-verification-container {
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ justify-content: center;
81
+ width: 100%;
82
+ min-height: 100vh;
83
+ margin: 0;
84
+ padding: 40px 20px;
85
+ text-align: center;
86
+ background-color: #2a2a2a;
87
+ position: fixed;
88
+ top: 0;
89
+ left: 0;
90
+ right: 0;
91
+ bottom: 0;
92
+ }
93
+
94
+ .safepassage-video-container {
95
+ position: relative;
96
+ width: 320px;
97
+ height: 320px;
98
+ margin-bottom: 40px;
99
+ }
100
+
101
+ .safepassage-verification-video {
102
+ width: 100%;
103
+ height: 100%;
104
+ object-fit: cover;
105
+ border-radius: 50%;
106
+ background-color: #000;
107
+ }
108
+
109
+ .safepassage-video-overlay {
110
+ position: absolute;
111
+ top: 0;
112
+ left: 0;
113
+ width: 100%;
114
+ height: 100%;
115
+ pointer-events: none;
116
+ }
117
+
118
+ .safepassage-oval-frame {
119
+ width: 100%;
120
+ height: 100%;
121
+ border: 3px solid #ffffff;
122
+ border-radius: 50%;
123
+ box-shadow:
124
+ 0 0 20px rgba(255, 255, 255, 0.3),
125
+ 0 0 calc(30px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.8)),
126
+ 0 0 calc(50px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.4)),
127
+ 0 0 calc(80px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.2));
128
+ transition: box-shadow 0.3s ease-out;
129
+ }
130
+
131
+ .safepassage-instructions-area {
132
+ width: 100%;
133
+ max-width: 400px;
134
+ }
135
+
136
+ .safepassage-instruction-text {
137
+ font-size: 18px;
138
+ font-weight: 500;
139
+ color: #ffffff;
140
+ text-align: center;
141
+ line-height: 1.4;
142
+ min-height: 50px;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ }
147
+
148
+ .safepassage-instruction-zone-transition {
149
+ color: #4fc3f7;
150
+ font-weight: 600;
151
+ animation: safepassage-pulse 0.5s ease-in-out;
152
+ }
153
+
154
+ .safepassage-instruction-feedback {
155
+ color: #ffffff;
156
+ }
157
+
158
+ .safepassage-instruction-processing {
159
+ color: #81c784;
160
+ }
161
+
162
+ .safepassage-instruction-default {
163
+ color: #ffffff;
164
+ opacity: 0.9;
165
+ }
166
+
167
+ .safepassage-error-message {
168
+ color: #e53e3e;
169
+ margin-top: 10px;
170
+ }
171
+
172
+ @keyframes safepassage-pulse {
173
+ 0% { opacity: 0.7; transform: scale(1); }
174
+ 50% { opacity: 1; transform: scale(1.02); }
175
+ 100% { opacity: 0.9; transform: scale(1); }
176
+ }
177
+
178
+ @media (max-width: 480px) {
179
+ .safepassage-verification-container {
180
+ padding: 20px 15px;
181
+ }
182
+
183
+ .safepassage-video-container {
184
+ width: 280px;
185
+ height: 280px;
186
+ margin-bottom: 30px;
187
+ }
188
+
189
+ .safepassage-instruction-text {
190
+ font-size: 16px;
191
+ min-height: 45px;
192
+ }
193
+ }
194
+ ` })] }));
195
+ };
196
+ export default SafePassageVerification;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * SafePassage SDK Type Definitions
3
+ */
4
+ export interface SafePassageConfig {
5
+ /**
6
+ * Public API key (starts with pk_live_ or pk_test_)
7
+ */
8
+ apiKey: string;
9
+ /**
10
+ * URL to redirect to after successful verification
11
+ * Must be pre-registered in dashboard
12
+ */
13
+ returnUrl: string;
14
+ /**
15
+ * URL to redirect to if user cancels verification
16
+ * Must be pre-registered in dashboard
17
+ */
18
+ cancelUrl: string;
19
+ /**
20
+ * Environment to use
21
+ * @default Auto-detected based on hostname
22
+ */
23
+ environment?: 'production' | 'staging' | 'development';
24
+ /**
25
+ * Verification mode
26
+ * @default 'redirect'
27
+ */
28
+ mode?: 'redirect' | 'new-tab';
29
+ /**
30
+ * Default challenge age (minimum 25)
31
+ * Can be overridden per verification
32
+ */
33
+ defaultChallengeAge?: number;
34
+ /**
35
+ * Default verification mode
36
+ * Can be overridden per verification
37
+ */
38
+ defaultVerificationMode?: 'L1' | 'L2';
39
+ /**
40
+ * Callback when verification completes (new-tab mode only)
41
+ */
42
+ onComplete?: (result: VerificationResult) => void;
43
+ /**
44
+ * Callback when user cancels (new-tab mode only)
45
+ */
46
+ onCancel?: () => void;
47
+ /**
48
+ * Callback for errors
49
+ */
50
+ onError?: (error: Error) => void;
51
+ }
52
+ export interface VerificationOptions {
53
+ /**
54
+ * Merchant-generated UUID v4 for this verification session
55
+ * Required for security - prevents session fixation attacks
56
+ */
57
+ sessionId: string;
58
+ /**
59
+ * Minimum age to verify (minimum 25)
60
+ * @default Uses merchant dashboard configuration
61
+ */
62
+ challengeAge?: number;
63
+ /**
64
+ * Verification mode
65
+ * L1: Age estimation allowed if user appears older
66
+ * L2: Full ID verification required
67
+ * @default Uses merchant dashboard configuration
68
+ */
69
+ verificationMode?: 'L1' | 'L2';
70
+ }
71
+ export interface VerificationResult {
72
+ /**
73
+ * The session ID that was verified
74
+ */
75
+ sessionId: string;
76
+ /**
77
+ * Binary result: 'verified' or 'failed'
78
+ * Full details available via server-side API
79
+ */
80
+ status: 'verified' | 'failed' | 'cancelled';
81
+ }
82
+ export interface StatePayload {
83
+ merchantId: string;
84
+ sessionId: string;
85
+ returnUrl: string;
86
+ cancelUrl: string;
87
+ challengeAge?: number;
88
+ verificationMode?: 'L1' | 'L2';
89
+ timestamp: number;
90
+ }
91
+ export interface SessionValidationResponse {
92
+ sessionId: string;
93
+ merchantId: string;
94
+ status: 'verified' | 'failed';
95
+ verified: boolean;
96
+ estimatedAge?: number;
97
+ challengeAge: number;
98
+ verificationMode: 'L1' | 'L2';
99
+ verificationMethod?: 'facial' | 'document' | 'combined';
100
+ timestamp: string;
101
+ expiresAt: string;
102
+ }
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // SafePassage SDK v3.0.0 - Redirect Implementation
2
+ // Main export for the new redirect-based SDK
3
+ export { SafePassage } from '../src-redirect/core/SafePassageSDK';
4
+ // Re-export for convenience
5
+ export * from '../src-redirect/types';
6
+ // Default export
7
+ export { SafePassage as default } from '../src-redirect/core/SafePassageSDK';
@@ -0,0 +1,3 @@
1
+ /* SafePassage SDK v3.0.0 - Redirect Implementation */
2
+ "use strict";var SafePassageSDK=(()=>{var p=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var c=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>{for(var n in e)p(t,n,{get:e[n],enumerable:!0})},q=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of H(e))!K.call(t,o)&&o!==n&&p(t,o,{get:()=>e[o],enumerable:!(i=W(e,o))||i.enumerable});return t};var I=t=>q(p({},"__esModule",{value:!0}),t);function j(t,e){return E[e].includes(t)}function P(t,e,n=[]){let{origin:i}=t;return j(i,e)||n.length>0&&n.some(r=>{if(r.startsWith("*.")){let a=r.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===r})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:E[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function T(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed","cancelled"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function U(t){if(t==="production"&&window.location.protocol!=="https:"){let e=window.location.href.replace("http:","https:");console.error("SafePassage Security: HTTPS required in production. Redirecting...",{current:window.location.href,redirect:e}),window.location.replace(e)}}function f(t,e){try{let n=new URL(t);if(e==="production"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in production"};if(e==="development"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"||n.hostname.endsWith(".local"))&&n.protocol!=="https:")return{isValid:!1,error:"Non-localhost URLs must use HTTPS"};if(e==="staging"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in staging"};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let o of i)if(o.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch{return{isValid:!1,error:"Invalid URL format"}}}function s(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var E,g,V,h=c(()=>{"use strict";E={production:["https://verify.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://verify-staging.safepassageapp.com","https://portal-staging.safepassageapp.com","https://api-staging.safepassageapp.com"],development:["http://localhost:5173","http://localhost:3000","http://localhost:3001","http://localhost:3002","http://127.0.0.1:5173","http://127.0.0.1:3000","http://127.0.0.1:3001","http://127.0.0.1:3002"]};g=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),o=(this.attempts.get(e)||[]).filter(r=>n-r<this.timeWindow);return o.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(o.push(n),this.attempts.set(e,o),!0)}reset(e){this.attempts.delete(e)}},V=new g});var L={};u(L,{createSignedState:()=>J,generateHMAC:()=>m,generateSecureToken:()=>b,getSigningSecret:()=>w,parseSignedState:()=>B,verifyHMAC:()=>A});async function m(t,e){let n=new TextEncoder,i=n.encode(e),o=n.encode(t),r=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),a=await crypto.subtle.sign("HMAC",r,o);return Array.from(new Uint8Array(a)).map(d=>d.toString(16).padStart(2,"0")).join("")}async function A(t,e,n){try{let i=await m(t,n);return F(e,i)}catch{return!1}}function F(t,e){if(t.length!==e.length)return!1;let n=0;for(let i=0;i<t.length;i++)n|=t.charCodeAt(i)^e.charCodeAt(i);return n===0}function b(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function w(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function J(t,e){let n={...t,timestamp:Date.now(),nonce:b(16)},i=JSON.stringify(n),o=w(e),r=await m(i,o);return btoa(JSON.stringify({data:n,signature:r}))}async function B(t,e,n=10*60*1e3){try{let i=atob(t),o=JSON.parse(i);if(!o.data||!o.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:r,signature:a}=o,d=JSON.stringify(r),k=w(e);if(!await A(d,a,k))return console.warn("SafePassage: State signature verification failed"),null;if(r.timestamp){let y=Date.now()-r.timestamp;if(y>n)return console.warn("SafePassage: State parameter expired",{age:y,maxAge:n}),null}let{timestamp:ne,nonce:ie,...$}=r;return $}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var x=c(()=>{"use strict"});function C(t){if(!t.apiKey)throw new Error("apiKey is required");if(!G.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx or sk_xxx");if(!t.returnUrl)throw new Error("returnUrl is required");if(!t.cancelUrl)throw new Error("cancelUrl is required");let e=Y(),n=f(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=f(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0&&t.defaultChallengeAge<M)throw new Error(`defaultChallengeAge must be at least ${M}`);if(t.defaultVerificationMode&&!["L1","L2"].includes(t.defaultVerificationMode))throw new Error("defaultVerificationMode must be L1 or L2");if(t.mode&&!["redirect","new-tab"].includes(t.mode))throw new Error("mode must be redirect or new-tab")}function Y(){let t=window.location.hostname;return t==="localhost"||t==="127.0.0.1"||t.includes(".local")?"development":t.includes("staging")||t.includes("stage")?"staging":"production"}async function R(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(x(),L));return n(t,e)}var M,G,O=c(()=>{"use strict";h();M=25,G=/^(pk|sk)_[a-zA-Z0-9]+$/});function v(t){let e=Z[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function z(t){let n={production:"https://api.safepassageapp.com",staging:"https://api-staging.safepassageapp.com",development:"http://localhost:3001"}[t];if((t==="production"||t==="staging")&&!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function D(t){let e=window.location.protocol==="https:",n=window.location.hostname;switch(t){case"production":if(!e)throw new Error("SafePassage requires HTTPS in production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break;case"development":let i=n==="localhost"||n==="127.0.0.1"||n.includes(".local");!e&&!i&&console.warn("SafePassage Warning: HTTPS recommended for non-localhost development");break}try{v(t),z(t)}catch(i){throw new Error(`Environment configuration validation failed: ${i}`)}}var Z,N=c(()=>{"use strict";Z={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var _={};u(_,{SafePassage:()=>l,default:()=>X});var l,X,S=c(()=>{"use strict";O();N();h();l=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;C(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},D(this.config.environment),U(this.config.environment),s("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e){if(!e.sessionId)throw new Error("sessionId is required - must be a merchant-generated UUID v4");if(this.isVerificationInProgress){let n=new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0,8)}...`);throw s("RACE_CONDITION_PREVENTED",{currentSession:this.currentSessionId?.substring(0,8)+"...",attemptedSession:e.sessionId.substring(0,8)+"...",origin:window.location.origin}),this.config.onError?.(n),n}this.isVerificationInProgress=!0,this.currentSessionId=e.sessionId;try{let n=`${this.config.apiKey}:${window.location.origin}`;if(!V.isAllowed(n)){let o=new Error("Too many verification attempts. Please wait before trying again.");throw s("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:e.sessionId.substring(0,8)+"..."}),this.config.onError?.(o),o}let i=await this.buildVerificationUrl(e);s("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:e.sessionId.substring(0,8)+"...",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(i,e.sessionId):(this.unlockVerification(),this.redirect(i))}catch(n){throw this.unlockVerification(),n}}async buildVerificationUrl(e){let n=v(this.config.environment),i=await R({merchantId:this.config.apiKey,sessionId:e.sessionId,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,timestamp:Date.now()},this.config.environment),o=new URLSearchParams({state:i,sessionId:e.sessionId,mode:this.config.mode});return`${n}/verify?${o.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){this.config.onError?.(new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=i=>{if(!P(i,this.config.environment)){s("POSTMESSAGE_ORIGIN_BLOCKED",{origin:i.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:i.data?.type});return}let o=T(i,n);if(!o.isValid){s("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:i.origin,sessionId:n.substring(0,8)+"...",messageType:i.data?.type});return}let r={sessionId:i.data.sessionId,status:i.data.status};s("VERIFICATION_COMPLETED",{status:r.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),r.status==="verified"?this.config.onComplete?.(r):r.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${r.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(s("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),this.config.onCancel?.())},500)}setupAutoCleanup(){if(this.unloadListener=()=>{s("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e==="localhost"||e==="127.0.0.1"||e.includes(".local")?"development":e.includes("staging")||e.includes("stage")?"staging":"production"}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,s("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){s("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}},X=l});var ee={};u(ee,{SafePassage:()=>l,VERSION:()=>Q,default:()=>l});S();var Q="3.0.0";typeof window<"u"&&window&&(window.SafePassage=(S(),I(_)).SafePassage);return I(ee);})();
3
+ if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@safepassage/sdk",
3
+ "version": "3.0.0",
4
+ "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc && node build-sdk.js",
14
+ "dev": "tsc --watch",
15
+ "demo": "python3 -m http.server 8080",
16
+ "serve-demo": "npx serve . -p 8080",
17
+ "test": "jest --config test/jest.config.js",
18
+ "test:browser": "jest --config test/jest.config.js test/browser-compat",
19
+ "test:mobile": "jest --config test/jest.config.js test/mobile",
20
+ "test:performance": "jest --config test/jest.config.js test/performance",
21
+ "test:security": "jest --config test/jest.config.js test/security",
22
+ "test:accessibility": "jest --config test/jest.config.js test/accessibility",
23
+ "test:edge": "jest --config test/jest.config.js test/edge-cases",
24
+ "test:integration": "jest --config test/jest.config.js test/integration",
25
+ "test:all": "node test/run-tests.js",
26
+ "test:coverage": "jest --config test/jest.config.js --coverage",
27
+ "benchmark": "node test/performance/benchmark.js",
28
+ "prepublishOnly": "npm run build",
29
+ "version": "npm run build && git add -A dist",
30
+ "postversion": "git push && git push --tags"
31
+ },
32
+ "keywords": [
33
+ "safepassage",
34
+ "age-verification",
35
+ "redirect",
36
+ "sdk",
37
+ "typescript",
38
+ "lightweight"
39
+ ],
40
+ "author": "SafePassage",
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "@babel/core": "^7.27.4",
44
+ "@babel/preset-env": "^7.27.2",
45
+ "@types/jest": "^29.5.12",
46
+ "@types/node": "^24.0.1",
47
+ "babel-jest": "^30.0.0",
48
+ "esbuild": "^0.25.5",
49
+ "jest": "^29.7.0",
50
+ "jest-environment-jsdom": "^29.7.0",
51
+ "puppeteer": "^21.11.0",
52
+ "terser": "^5.42.0",
53
+ "ts-jest": "^29.1.2",
54
+ "typescript": "~5.8.3"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/safepassage/safepassage-monorepo",
59
+ "directory": "services/verify-ui/sdk"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/safepassage/safepassage-monorepo/issues"
63
+ },
64
+ "homepage": "https://safepassageapp.com/docs/sdk",
65
+ "publishConfig": {
66
+ "access": "public",
67
+ "registry": "https://registry.npmjs.org/"
68
+ },
69
+ "engines": {
70
+ "node": ">=14.0.0"
71
+ }
72
+ }