@joonweb/joonweb-sdk 1.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/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Main SDK entry point
2
+ module.exports = {
3
+ JoonWebAPI: require('./src/JoonWebAPI'),
4
+ Context: require('./src/Context'),
5
+ Helper: require('./src/Helper'),
6
+ OAuth: require('./src/Auth/OAuth'),
7
+ SessionManager: require('./src/Auth/SessionManager')
8
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@joonweb/joonweb-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official Node.js SDK for JoonWeb API",
5
+ "main": "index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "test:watch": "jest --watch",
12
+ "lint": "eslint src/",
13
+ "format": "prettier --write \"src/**/*.js\"",
14
+ "example": "node examples/express-app.js",
15
+ "prepare": "npm run build",
16
+ "build": "echo 'No build step required for JavaScript SDK'"
17
+ },
18
+ "keywords": [
19
+ "joonweb",
20
+ "api",
21
+ "sdk",
22
+ "ecommerce",
23
+ "oauth",
24
+ "rest-api"
25
+ ],
26
+ "author": "Joonweb <business@jooncorporation.com>",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/JoonWebdotcom/joonweb-node-sdk.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/JoonWebdotcom/joonweb-node-sdk/issues"
34
+ },
35
+ "homepage": "https://github.com/JoonWebdotcom/joonweb-node-sdk#readme",
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ },
39
+ "dependencies": {
40
+ "axios": "^1.6.0"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^29.7.0",
44
+ "eslint": "^8.53.0",
45
+ "prettier": "^3.0.3",
46
+ "dotenv": "^16.3.0",
47
+ "express": "^4.18.2",
48
+ "express-session": "^1.17.3"
49
+ },
50
+ "optionalDependencies": {
51
+ "sqlite3": "^5.1.7",
52
+ "mongodb": "^6.3.0",
53
+ "redis": "^4.6.13"
54
+ },
55
+ "files": [
56
+ "src/",
57
+ "LICENSE",
58
+ "README.md"
59
+ ]
60
+ }
@@ -0,0 +1,193 @@
1
+ const Helper = require('../Helper');
2
+ const Context = require('../Context');
3
+ const crypto = require('crypto');
4
+
5
+ class Auth {
6
+ static STATE_COOKIE_NAME = 'joonweb_oauth_state';
7
+ static STATE_SIG_COOKIE_NAME = 'joonweb_oauth_state_sig';
8
+ static SESSION_COOKIE_NAME = 'joonweb_session_id';
9
+ static SESSION_SIG_COOKIE_NAME = 'joonweb_session_id_sig';
10
+
11
+ /**
12
+ * Begin OAuth flow
13
+ */
14
+ static beginAuth(site, redirectUri, scopes, isOnline = true) {
15
+ Context.throwIfUninitialized();
16
+
17
+ const state = crypto.randomBytes(16).toString('hex');
18
+ const scope = Array.isArray(scopes) ? scopes.join(',') : scopes;
19
+
20
+ // Generate authorization URL
21
+ const authUrl = `https://${site}/admin/oauth/authorize?` + new URLSearchParams({
22
+ client_id: Context.API_KEY,
23
+ scope: scope,
24
+ redirect_uri: redirectUri,
25
+ state: state,
26
+ 'grant_options[]': isOnline ? 'per-user' : ''
27
+ }).toString();
28
+
29
+ return {
30
+ url: authUrl,
31
+ state: state
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Validate OAuth callback
37
+ */
38
+ static async validateAuthCallback(query, cookies, setCookieFunction = null) {
39
+ Context.throwIfUninitialized();
40
+
41
+ const { site, code, state, hmac, timestamp } = query;
42
+
43
+ // Validate HMAC
44
+ if (!Helper.verifyHmac(query, Context.API_SECRET_KEY)) {
45
+ throw new Error('Invalid HMAC');
46
+ }
47
+
48
+ // Validate state from cookies
49
+ const stateCookie = this._getCookie(cookies, this.STATE_COOKIE_NAME, this.STATE_SIG_COOKIE_NAME);
50
+ if (!stateCookie || stateCookie !== state) {
51
+ throw new Error('Invalid OAuth state');
52
+ }
53
+
54
+ // Exchange code for token
55
+ const tokenData = await this._exchangeCodeForToken(code, site);
56
+
57
+ // Create session
58
+ const session = {
59
+ site: site,
60
+ accessToken: tokenData.access_token,
61
+ scope: tokenData.scope,
62
+ expiresAt: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
63
+ isOnline: !!tokenData.associated_user,
64
+ user: tokenData.associated_user || null
65
+ };
66
+
67
+ // Store session if storage is configured
68
+ if (Context.SESSION_STORAGE) {
69
+ await Context.SESSION_STORAGE.storeSession(session);
70
+ }
71
+
72
+ // Set session cookie for non-embedded apps
73
+ if (!Context.IS_EMBEDDED_APP && setCookieFunction) {
74
+ this._setSessionCookie(session, setCookieFunction);
75
+ }
76
+
77
+ return session;
78
+ }
79
+
80
+ /**
81
+ * Get current session from request
82
+ */
83
+ static async getCurrentSession(headers, cookies) {
84
+ Context.throwIfUninitialized();
85
+
86
+ if (Context.IS_EMBEDDED_APP) {
87
+ // Get session from JWT (embedded app)
88
+ const authHeader = headers['authorization'];
89
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
90
+ return null;
91
+ }
92
+
93
+ const jwt = authHeader.split(' ')[1];
94
+ const payload = Helper.decodeSessionToken(jwt);
95
+
96
+ if (!payload) {
97
+ return null;
98
+ }
99
+
100
+ // Extract site from JWT
101
+ const site = payload.dest ? payload.dest.replace('https://', '') : null;
102
+ if (!site) {
103
+ return null;
104
+ }
105
+
106
+ // Load session from storage
107
+ if (Context.SESSION_STORAGE) {
108
+ return await Context.SESSION_STORAGE.loadSession(site);
109
+ }
110
+
111
+ } else {
112
+ // Get session from cookies (non-embedded)
113
+ const sessionId = this._getCookie(cookies, this.SESSION_COOKIE_NAME, this.SESSION_SIG_COOKIE_NAME);
114
+ if (!sessionId || !Context.SESSION_STORAGE) {
115
+ return null;
116
+ }
117
+
118
+ return await Context.SESSION_STORAGE.loadSessionById(sessionId);
119
+ }
120
+
121
+ return null;
122
+ }
123
+
124
+ /**
125
+ * Exchange authorization code for access token
126
+ */
127
+ static async _exchangeCodeForToken(code, site) {
128
+ const url = `https://${site}/admin/oauth/access_token`;
129
+
130
+ const response = await fetch(url, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'Accept': 'application/json'
135
+ },
136
+ body: JSON.stringify({
137
+ client_id: Context.API_KEY,
138
+ client_secret: Context.API_SECRET_KEY,
139
+ code: code
140
+ })
141
+ });
142
+
143
+ if (!response.ok) {
144
+ throw new Error(`Token exchange failed: ${response.status}`);
145
+ }
146
+
147
+ return await response.json();
148
+ }
149
+
150
+ /**
151
+ * Get and validate cookie
152
+ */
153
+ static _getCookie(cookies, name, sigName) {
154
+ const cookieValue = cookies[name];
155
+ const signature = cookies[sigName];
156
+
157
+ if (!cookieValue || !signature) {
158
+ return null;
159
+ }
160
+
161
+ // Validate signature
162
+ const expectedSig = crypto
163
+ .createHmac('sha256', Context.API_SECRET_KEY)
164
+ .update(cookieValue)
165
+ .digest('hex');
166
+
167
+ if (signature !== expectedSig) {
168
+ return null;
169
+ }
170
+
171
+ return cookieValue;
172
+ }
173
+
174
+ /**
175
+ * Set session cookie with signature
176
+ */
177
+ static _setSessionCookie(session, setCookieFunction) {
178
+ const sessionId = session.id || crypto.randomBytes(16).toString('hex');
179
+ const signature = crypto
180
+ .createHmac('sha256', Context.API_SECRET_KEY)
181
+ .update(sessionId)
182
+ .digest('hex');
183
+
184
+ const expires = session.expiresAt ? new Date(session.expiresAt) : undefined;
185
+
186
+ if (setCookieFunction) {
187
+ setCookieFunction(this.SESSION_COOKIE_NAME, sessionId, { expires, httpOnly: true });
188
+ setCookieFunction(this.SESSION_SIG_COOKIE_NAME, signature, { expires, httpOnly: true });
189
+ }
190
+ }
191
+ }
192
+
193
+ module.exports = Auth;
@@ -0,0 +1,135 @@
1
+ const Helper = require('../Helper');
2
+ const SessionManager = require('./SessionManager');
3
+ const crypto = require('crypto');
4
+
5
+ class OAuth {
6
+ constructor(config = {}) {
7
+ this.clientId = config.clientId || process.env.JOONWEB_CLIENT_ID || 'test_client';
8
+ this.clientSecret = config.clientSecret || process.env.JOONWEB_CLIENT_SECRET || 'test_secret';
9
+ this.redirectUri = config.redirectUri || process.env.JOONWEB_REDIRECT_URI || 'http://localhost:3000/auth/callback';
10
+ this.scopes = config.scopes || ['read_products', 'write_products'];
11
+ this.sessionManager = config.sessionManager || new SessionManager();
12
+ this.appSlug = config.appSlug || process.env.JOONWEB_CLIENT_ID || 'test-app';
13
+ }
14
+
15
+ /**
16
+ * Generate OAuth URL
17
+ */
18
+ beginAuth(siteDomain) {
19
+ const state = crypto.randomBytes(16).toString('hex');
20
+
21
+ const params = new URLSearchParams({
22
+ client_id: this.clientId,
23
+ scope: this.scopes.join(','),
24
+ redirect_uri: this.redirectUri,
25
+ state: state,
26
+ site: siteDomain
27
+ });
28
+
29
+ return {
30
+ url: `https://accounts.joonweb.com/oauth/authorize?${params.toString()}`,
31
+ state: state
32
+ };
33
+ }
34
+ async exchangeCodeForToken(code, siteDomain) {
35
+ const JoonWebAPI = require('../JoonWebAPI');
36
+ const api = new JoonWebAPI();
37
+
38
+ const tokenData = await api.exchangeCodeForToken(code, siteDomain, {
39
+ client_id: this.clientId,
40
+ client_secret: this.clientSecret,
41
+ redirect_uri: this.redirectUri
42
+ });
43
+
44
+ return tokenData;
45
+ }
46
+ /**
47
+ * Validate OAuth callback
48
+ */
49
+ async validateAuthCallback(query, sessionState) {
50
+
51
+ // For testing, skip HMAC verification
52
+ if (!this.verifyHmac(query)) {
53
+ throw new Error('Invalid HMAC signature');
54
+ }
55
+
56
+ const { code, site } = query;
57
+
58
+ // Create API instance
59
+ const JoonWebAPI = require('../JoonWebAPI');
60
+ const api = new JoonWebAPI();
61
+
62
+ // Exchange code for token
63
+ const tokenData = await api.exchangeCodeForToken(code, site);
64
+
65
+ // Start session
66
+ await this.sessionManager.startSession(site, tokenData);
67
+
68
+ // Set API credentials
69
+ api.setAccessToken(tokenData.access_token)
70
+ .setSiteDomain(site);
71
+
72
+ // Get site info
73
+ const siteInfo = await api.site.get();
74
+
75
+ // Save data
76
+ await this.sessionManager.saveAppData(site, 'site_info', siteInfo.site);
77
+
78
+ await this.sessionManager.trackAnalytics(site, 'installation_completed', {
79
+ site_name: siteInfo.site.name,
80
+ email: siteInfo.site.email
81
+ });
82
+
83
+ return {
84
+ site: site,
85
+ accessToken: tokenData.access_token,
86
+ scope: tokenData.scope,
87
+ expiresAt: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
88
+ user: tokenData.associated_user || null,
89
+ siteInfo: siteInfo.site
90
+ };
91
+ }
92
+
93
+ verifyHmac(params) {
94
+ // For testing, always return true
95
+ return true;
96
+
97
+ // Real implementation would verify HMAC here
98
+ }
99
+
100
+ getEmbedUrl(siteDomain, siteHash = '', app_slug = null) {
101
+ return `https://accounts.joonweb.com/site/?sitehash=${siteHash}&apps&${encodeURIComponent(app_slug)}`;
102
+ }
103
+
104
+ /**
105
+ * Complete OAuth flow handler
106
+ */
107
+ async handleCallback(req, res) {
108
+ try {
109
+
110
+ const session = await this.validateAuthCallback(req.query, req.session.oauth_state);
111
+
112
+ // Build embed URL
113
+ const embedUrl = this.getEmbedUrl(session.site, req.query.site_hash, req.query.app_slug);
114
+ // Redirect to embedded app
115
+ res.redirect(embedUrl);
116
+
117
+ } catch (error) {
118
+ console.error('OAuth Error:', error);
119
+
120
+ if (req.query.site) {
121
+ await this.sessionManager.trackAnalytics(req.query.site, 'installation_failed', {
122
+ error: error.message
123
+ });
124
+ }
125
+
126
+ res.status(400).send(`
127
+ <h1>❌ Installation Failed</h1>
128
+ <p>Error: ${error.message}</p>
129
+ <p><a href="/auth?site=${req.query.site || ''}">Try Again</a></p>
130
+ `);
131
+ }
132
+ }
133
+ }
134
+
135
+ module.exports = OAuth;
@@ -0,0 +1,145 @@
1
+ // src/Auth/SessionManager.js
2
+ const crypto = require('crypto');
3
+
4
+ class SessionManager {
5
+ constructor(storageAdapter = null) {
6
+ // Use provided adapter or default to MemoryStorage
7
+ if (storageAdapter) {
8
+ this.storage = storageAdapter;
9
+ } else {
10
+ const { MemoryStorage } = require('./session/storage');
11
+ this.storage = new MemoryStorage();
12
+ }
13
+ }
14
+
15
+ async startSession(siteDomain, tokenData, additionalData = {}) {
16
+ const session = {
17
+ site_domain: siteDomain,
18
+ access_token: tokenData.access_token,
19
+ scope: tokenData.scope,
20
+ expires_at: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
21
+ user: tokenData.associated_user || null,
22
+ authenticated_at: Date.now(),
23
+ ...additionalData
24
+ };
25
+
26
+ await this.storage.store(siteDomain, session);
27
+ return session;
28
+ }
29
+
30
+ async isAuthenticated(siteDomain) {
31
+ const session = await this.storage.load(siteDomain);
32
+ if (!session) return false;
33
+
34
+ if (session.expires_at && session.expires_at < Date.now()) {
35
+ await this.destroySession(siteDomain);
36
+ return false;
37
+ }
38
+
39
+ return true;
40
+ }
41
+
42
+ async getAccessToken(siteDomain) {
43
+ const session = await this.storage.load(siteDomain);
44
+ return session ? session.access_token : null;
45
+ }
46
+
47
+ async getSessionData(siteDomain) {
48
+ return await this.storage.load(siteDomain);
49
+ }
50
+
51
+ getSiteDomain(siteDomain) {
52
+ return siteDomain; // This method seems redundant
53
+ }
54
+
55
+ async saveAppData(siteDomain, key, data) {
56
+ // Load existing session
57
+ const session = await this.getSessionData(siteDomain);
58
+ if (!session) return false;
59
+
60
+ // Add metadata
61
+ session.metadata = session.metadata || {};
62
+ session.metadata[key] = data;
63
+
64
+ // Save back
65
+ await this.storage.store(siteDomain, session);
66
+ console.log(`✅ Saved ${key} for ${siteDomain}`);
67
+ return true;
68
+ }
69
+
70
+ async trackAnalytics(siteDomain, event, data = {}) {
71
+ // Load existing session
72
+ const session = await this.getSessionData(siteDomain);
73
+ if (!session) return false;
74
+
75
+ // Add analytics data
76
+ session.metadata = session.metadata || {};
77
+ session.metadata.analytics = session.metadata.analytics || [];
78
+ session.metadata.analytics.push({
79
+ event,
80
+ data,
81
+ timestamp: Date.now()
82
+ });
83
+
84
+ // Save back
85
+ await this.storage.store(siteDomain, session);
86
+ console.log(`📊 Tracked ${event} for ${siteDomain}`);
87
+ return true;
88
+ }
89
+
90
+ isEmbeddedRequest(headers) {
91
+ return (
92
+ (headers['sec-fetch-dest'] === 'iframe') ||
93
+ (headers.referer && headers.referer.includes('joonweb.com'))
94
+ );
95
+ }
96
+
97
+ async destroySession(siteDomain) {
98
+ return await this.storage.delete(siteDomain);
99
+ }
100
+
101
+ // New methods for better session management
102
+ async refreshSession(siteDomain, newTokenData) {
103
+ const existingSession = await this.getSessionData(siteDomain);
104
+ if (!existingSession) return null;
105
+
106
+ const updatedSession = {
107
+ ...existingSession,
108
+ access_token: newTokenData.access_token,
109
+ expires_at: newTokenData.expires_in ? Date.now() + (newTokenData.expires_in * 1000) : null,
110
+ updated_at: Date.now()
111
+ };
112
+
113
+ await this.storage.store(siteDomain, updatedSession);
114
+ return updatedSession;
115
+ }
116
+
117
+ async listAllSessions() {
118
+ return await this.storage.list();
119
+ }
120
+
121
+ async clearAllSessions() {
122
+ return await this.storage.clear();
123
+ }
124
+
125
+ async getSessionCount() {
126
+ const sessions = await this.listAllSessions();
127
+ return sessions.length;
128
+ }
129
+
130
+ // Utility method to get active sessions
131
+ async getActiveSessions() {
132
+ const allSessions = await this.listAllSessions();
133
+ const activeSessions = [];
134
+
135
+ for (const session of allSessions) {
136
+ if (await this.isAuthenticated(session.site_domain)) {
137
+ activeSessions.push(session);
138
+ }
139
+ }
140
+
141
+ return activeSessions;
142
+ }
143
+ }
144
+
145
+ module.exports = SessionManager;
@@ -0,0 +1,46 @@
1
+ class Session {
2
+ constructor(id, siteDomain, accessToken) {
3
+ this.id = id;
4
+ this.site_domain = siteDomain;
5
+ this.access_token = accessToken;
6
+ this.scope = '';
7
+ this.user = null;
8
+ this.expires_at = null;
9
+ this.created_at = Date.now();
10
+ this.updated_at = Date.now();
11
+ this.is_online = false;
12
+ this.online_access_info = {};
13
+ this.state = '';
14
+ this.metadata = {};
15
+ }
16
+
17
+ isValid() {
18
+ if (!this.expires_at) return true;
19
+ return this.expires_at > Date.now();
20
+ }
21
+
22
+ toJSON() {
23
+ return {
24
+ id: this.id,
25
+ site_domain: this.site_domain,
26
+ access_token: this.access_token,
27
+ scope: this.scope,
28
+ user: this.user,
29
+ expires_at: this.expires_at,
30
+ created_at: this.created_at,
31
+ updated_at: this.updated_at,
32
+ is_online: this.is_online,
33
+ online_access_info: this.online_access_info,
34
+ state: this.state,
35
+ metadata: this.metadata
36
+ };
37
+ }
38
+
39
+ static fromJSON(data) {
40
+ const session = new Session(data.id, data.site_domain, data.access_token);
41
+ Object.assign(session, data);
42
+ return session;
43
+ }
44
+ }
45
+
46
+ module.exports = Session;
@@ -0,0 +1,48 @@
1
+ class StorageInterface {
2
+ /**
3
+ * Store a JoonWeb session
4
+ * @param {Session} session
5
+ * @returns {Promise<boolean>}
6
+ */
7
+ async store(session) {
8
+ throw new Error('Method not implemented: store');
9
+ }
10
+
11
+ /**
12
+ * Load a session by ID
13
+ * @param {string} sessionId
14
+ * @returns {Promise<Session|null>}
15
+ */
16
+ async load(sessionId) {
17
+ throw new Error('Method not implemented: load');
18
+ }
19
+
20
+ /**
21
+ * Delete a session
22
+ * @param {string} sessionId
23
+ * @returns {Promise<boolean>}
24
+ */
25
+ async delete(sessionId) {
26
+ throw new Error('Method not implemented: delete');
27
+ }
28
+
29
+ /**
30
+ * Find sessions by site domain
31
+ * @param {string} siteDomain
32
+ * @returns {Promise<Session[]>}
33
+ */
34
+ async findBySiteDomain(siteDomain) {
35
+ throw new Error('Method not implemented: findBySiteDomain');
36
+ }
37
+
38
+ /**
39
+ * Find sessions by query
40
+ * @param {Object} query
41
+ * @returns {Promise<Session[]>}
42
+ */
43
+ async findByQuery(query) {
44
+ throw new Error('Method not implemented: findByQuery');
45
+ }
46
+ }
47
+
48
+ module.exports = StorageInterface;
@@ -0,0 +1,34 @@
1
+ // src/session/storage/MemoryStorage.js
2
+ class MemoryStorage {
3
+ constructor() {
4
+ this.sessions = new Map();
5
+ }
6
+
7
+ async store(siteDomain, session) {
8
+ this.sessions.set(siteDomain, session);
9
+ return true;
10
+ }
11
+
12
+ async load(siteDomain) {
13
+ return this.sessions.get(siteDomain) || null;
14
+ }
15
+
16
+ async delete(siteDomain) {
17
+ return this.sessions.delete(siteDomain);
18
+ }
19
+
20
+ async list() {
21
+ return Array.from(this.sessions.values());
22
+ }
23
+
24
+ async clear() {
25
+ this.sessions.clear();
26
+ return true;
27
+ }
28
+
29
+ async count() {
30
+ return this.sessions.size;
31
+ }
32
+ }
33
+
34
+ module.exports = MemoryStorage;