@makefully/adaptfully 3.0.2 → 3.1.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/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 3.1.0 — 2026-06-18
6
+
7
+ ### Added
8
+
9
+ - Built-in storage plugins: `localStorage` (sync) and `indexedDB` (async).
10
+ - Register with `"storage": "localStorage"` or `"storage": "indexedDB"` in `config.platforms.<platform>.registrations`.
11
+
12
+ ### Changed
13
+
14
+ - Auth helpers use registered `storage` only (no implicit fallbacks).
15
+
16
+ ## 3.0.3 — 2026-06-18
17
+
18
+ ### Added
19
+
20
+ - `platform.autoLogin()` and `platform.supportsAutoLogin()` for session restore without UI.
21
+ - Google auth reads `config.autoLoginStorageKey` (default: `lastLoggedIn`) when deciding whether to attempt silent OAuth.
22
+
23
+ ### Changed
24
+
25
+ - Auth plugins expose `autoLogin()` only (removed `silentLogin()`).
26
+
27
+ ### Fixed
28
+
29
+ - Google auth reads OAuth config and stored tokens lazily so bridge-registered `config`/`storage` are applied before auto-login runs.
30
+ - Prebuild injects deploy bridge scripts after `core.js` and before auth plugin runtime scripts.
31
+ - Auth storage helper falls back to `localStorage` when `storage` is not registered via Adaptfully.
32
+
5
33
  ## 3.0.2 — 2026-06-18
6
34
 
7
35
  ### Fixed
package/README.md CHANGED
@@ -45,6 +45,21 @@ const platform = adaptfully.get('auth');
45
45
  platform.login(function (result) { /* ... */ });
46
46
  ```
47
47
 
48
+ ### Storage plugins
49
+
50
+ | Plugin key | Registration | Runtime |
51
+ |------------|--------------|---------|
52
+ | `localStorage` | `adaptfully.register('storage', adaptfully.storage.LocalStorage())` | Sync key/value storage in the browser |
53
+ | `indexedDB` | `adaptfully.register('storage', adaptfully.storage.IndexedDB())` | Async key/value storage for larger payloads |
54
+
55
+ In-game:
56
+
57
+ ```javascript
58
+ const storage = adaptfully.get('storage');
59
+ storage.set('playerName', 'Ada');
60
+ storage.getObject('currentGame');
61
+ ```
62
+
48
63
  ### Auth plugins
49
64
 
50
65
  | Plugin key | Registration | Runtime |
@@ -61,18 +76,19 @@ Use plugin keys in `config.platforms.<platform>.registrations`. Custom deploy sc
61
76
  "platforms": {
62
77
  "steam": {
63
78
  "registrations": {
64
- "auth": "steam-auth",
65
- "storage": "/javascript/custom-storage-solution.js"
79
+ "storage": "localStorage",
80
+ "auth": "steam-auth"
66
81
  }
67
82
  },
68
83
  "web": {
69
84
  "registrations": {
70
- "auth": "google-auth",
71
- "storage": "javascript/adaptfully-bridge.js"
85
+ "storage": "localStorage",
86
+ "auth": "google-auth"
72
87
  }
73
88
  },
74
89
  "dev": {
75
90
  "registrations": {
91
+ "storage": "localStorage",
76
92
  "auth": "dev-auth"
77
93
  }
78
94
  }
@@ -107,6 +123,8 @@ import {
107
123
  - **`prebuildPlatform(deployFolder, platformKey, pkg)`** — copy `deploy/` to `output/<platform>-prebuild/` and inject registrations into `config.htmlInjections` (default: `index.html`).
108
124
  - **`resolveRegistrationAssets(registrations)`** — resolve runtime script paths, inline registration JS, and external script tags for a registration map (useful for Vite dev servers).
109
125
  - **`runAdaptfullyStage('prebuild' | 'build' | 'deploy', platformKey, options)`** — run a pipeline stage programmatically.
126
+ - **`platform.autoLogin(callback)`** — restore a prior session without UI when the auth plugin supports it (Google uses `lastLoggedIn` in storage and a cached OAuth token).
127
+ - **`platform.supportsAutoLogin()`** — whether the active auth plugin can attempt automatic sign-in.
110
128
 
111
129
  ---
112
130
 
@@ -25,6 +25,14 @@ export const STANDARD_PLUGINS = {
25
25
  scripts: ['core.js', 'platform.js', 'auth/_helpers.js', 'auth/dev-auth.js'],
26
26
  registration: (key) => `adaptfully.register('${key}', adaptfully.auth.Dev);`,
27
27
  },
28
+ localStorage: {
29
+ scripts: ['core.js', 'storage/_helpers.js', 'storage/local-storage.js'],
30
+ registration: (key) => `adaptfully.register('${key}', adaptfully.storage.LocalStorage());`,
31
+ },
32
+ indexedDB: {
33
+ scripts: ['core.js', 'storage/_helpers.js', 'storage/indexed-db.js'],
34
+ registration: (key) => `adaptfully.register('${key}', adaptfully.storage.IndexedDB());`,
35
+ },
28
36
  };
29
37
 
30
38
  /** Default Wrapfully builder → config.platforms key */
@@ -201,7 +209,11 @@ export function buildAdaptfullyInjection(registrations, options = {}) {
201
209
  block += `${extScript}\n`;
202
210
  }
203
211
 
204
- for (const script of parts.runtimeScripts) {
212
+ const coreScript = 'core.js';
213
+ const bootstrapScripts = parts.runtimeScripts.filter((script) => script === coreScript);
214
+ const pluginScripts = parts.runtimeScripts.filter((script) => script !== coreScript);
215
+
216
+ for (const script of bootstrapScripts) {
205
217
  block += `<script>\n${readRuntimeScript(script)}\n</script>\n`;
206
218
  }
207
219
 
@@ -209,6 +221,10 @@ export function buildAdaptfullyInjection(registrations, options = {}) {
209
221
  block += `<script src="${src}"></script>\n`;
210
222
  }
211
223
 
224
+ for (const script of pluginScripts) {
225
+ block += `<script>\n${readRuntimeScript(script)}\n</script>\n`;
226
+ }
227
+
212
228
  if (parts.inlineRegistrations.length > 0) {
213
229
  block += `<script>\n${parts.inlineRegistrations.join('\n')}\n</script>\n`;
214
230
  }
@@ -9,7 +9,8 @@
9
9
  * @property {string} name
10
10
  * @property {(done: ReadyCallback) => void} whenReady
11
11
  * @property {(callback: AuthCallback) => void} login
12
- * @property {(callback: AuthCallback) => void} silentLogin
12
+ * @property {(callback: AuthCallback) => void} autoLogin
13
+ * @property {() => boolean} [supportsAutoLogin]
13
14
  * @property {(callback: () => void) => void} logout
14
15
  * @property {() => AuthUser | null} getUser
15
16
  * @property {() => boolean} isAuthenticated
@@ -50,10 +50,14 @@
50
50
  });
51
51
  }
52
52
 
53
- silentLogin(callback) {
53
+ autoLogin(callback) {
54
54
  callback({ authenticated: this.authenticated, user: this.getUser() });
55
55
  }
56
56
 
57
+ supportsAutoLogin() {
58
+ return true;
59
+ }
60
+
57
61
  logout(callback) {
58
62
  const storage = getStorage();
59
63
  this.authenticated = false;
@@ -6,24 +6,42 @@
6
6
  const DEFAULT_CLIENT_ID = '225754014403.apps.googleusercontent.com';
7
7
  const DEFAULT_SCOPES = 'openid email profile';
8
8
  const DEFAULT_TOKEN_KEY = 'adaptfully_google_token';
9
+ const DEFAULT_AUTO_LOGIN_KEY = 'lastLoggedIn';
9
10
 
10
11
  class GoogleAuthPlugin {
11
12
  constructor() {
12
13
  this.name = 'google';
13
14
  this.tokenClient = null;
14
- this.tokenKey = configValue('googleTokenKey', DEFAULT_TOKEN_KEY);
15
- this.clientId = configValue('googleClientId', DEFAULT_CLIENT_ID);
16
- this.scopes = configValue('googleScopes', DEFAULT_SCOPES);
17
- this.accessToken = sessionStorage.getItem(this.tokenKey) || '';
15
+ this.accessToken = '';
18
16
  this.user = null;
19
17
  this.authenticated = false;
20
18
  }
21
19
 
20
+ supportsAutoLogin() {
21
+ return true;
22
+ }
23
+
24
+ #tokenKey() {
25
+ return configValue('googleTokenKey', DEFAULT_TOKEN_KEY);
26
+ }
27
+
28
+ #clientId() {
29
+ return configValue('googleClientId', DEFAULT_CLIENT_ID);
30
+ }
31
+
32
+ #scopes() {
33
+ return configValue('googleScopes', DEFAULT_SCOPES);
34
+ }
35
+
36
+ #autoLoginStorageKey() {
37
+ return configValue('autoLoginStorageKey', DEFAULT_AUTO_LOGIN_KEY);
38
+ }
39
+
22
40
  whenReady(done) {
23
41
  const setup = () => {
24
42
  this.tokenClient = google.accounts.oauth2.initTokenClient({
25
- client_id: this.clientId,
26
- scope: this.scopes,
43
+ client_id: this.#clientId(),
44
+ scope: this.#scopes(),
27
45
  callback: () => {},
28
46
  });
29
47
  done();
@@ -60,12 +78,19 @@
60
78
  this.accessToken = '';
61
79
  this.user = null;
62
80
  this.authenticated = false;
63
- sessionStorage.removeItem(this.tokenKey);
81
+ sessionStorage.removeItem(this.#tokenKey());
82
+ }
83
+
84
+ #loadStoredToken() {
85
+ if (!this.accessToken) {
86
+ this.accessToken = sessionStorage.getItem(this.#tokenKey()) || '';
87
+ }
64
88
  }
65
89
 
66
90
  #hasPersistedLogin() {
67
91
  const storage = getStorage();
68
- return !!(storage?.get('lastLoggedIn'));
92
+ const key = this.#autoLoginStorageKey();
93
+ return !!(storage?.get(key));
69
94
  }
70
95
 
71
96
  #fetchUserInfo(token, callback) {
@@ -80,7 +105,7 @@
80
105
  })
81
106
  .then((data) => {
82
107
  this.accessToken = token;
83
- sessionStorage.setItem(this.tokenKey, token);
108
+ sessionStorage.setItem(this.#tokenKey(), token);
84
109
  this.#applyUserInfo(data);
85
110
  callback();
86
111
  })
@@ -103,6 +128,8 @@
103
128
  }
104
129
 
105
130
  #restoreSession(callback) {
131
+ this.#loadStoredToken();
132
+
106
133
  const trySilentGoogleLogin = () => {
107
134
  if (this.#hasPersistedLogin()) {
108
135
  this.#requestAccessToken('none', callback);
@@ -135,7 +162,7 @@
135
162
  });
136
163
  }
137
164
 
138
- silentLogin(callback) {
165
+ autoLogin(callback) {
139
166
  if (this.authenticated && this.user?.id) {
140
167
  callback({ authenticated: true, user: this.getUser() });
141
168
  return;
@@ -149,7 +176,7 @@
149
176
  const storage = getStorage();
150
177
  const finish = () => {
151
178
  this.#clearSession();
152
- storage?.remove('lastLoggedIn');
179
+ storage?.remove(this.#autoLoginStorageKey());
153
180
  callback();
154
181
  };
155
182
 
@@ -22,10 +22,14 @@
22
22
  callback({ authenticated: this.authenticated, user: this.getUser() });
23
23
  }
24
24
 
25
- silentLogin(callback) {
25
+ autoLogin(callback) {
26
26
  callback({ authenticated: this.authenticated, user: this.getUser() });
27
27
  }
28
28
 
29
+ supportsAutoLogin() {
30
+ return false;
31
+ }
32
+
29
33
  logout(callback) {
30
34
  const storage = getStorage();
31
35
  this.user = null;
@@ -53,13 +53,17 @@
53
53
  });
54
54
  }
55
55
 
56
- silentLogin(callback) {
56
+ autoLogin(callback) {
57
57
  this.whenReady(() => {
58
58
  if (!this.online) {
59
59
  callback({ authenticated: false });
60
60
  return;
61
61
  }
62
- this.auth.silentLogin((result) => {
62
+ if (typeof this.auth.autoLogin !== 'function') {
63
+ callback({ authenticated: false });
64
+ return;
65
+ }
66
+ this.auth.autoLogin((result) => {
63
67
  callback(result || {
64
68
  authenticated: this.auth.isAuthenticated(),
65
69
  user: this.auth.getUser(),
@@ -68,6 +72,12 @@
68
72
  });
69
73
  }
70
74
 
75
+ supportsAutoLogin() {
76
+ return typeof this.auth.supportsAutoLogin === 'function'
77
+ ? this.auth.supportsAutoLogin()
78
+ : typeof this.auth.autoLogin === 'function';
79
+ }
80
+
71
81
  logout(callback) {
72
82
  this.whenReady(() => {
73
83
  this.auth.logout(() => {
@@ -0,0 +1,5 @@
1
+ /* global window */
2
+
3
+ (function registerStorageHelpers(ns) {
4
+ ns.storage = ns.storage || {};
5
+ }(window.adaptfully));
@@ -0,0 +1,87 @@
1
+ /* global indexedDB, window */
2
+
3
+ (function registerIndexedDBStorage(ns) {
4
+ function configValue(key, fallback) {
5
+ if (!ns.has('config')) {
6
+ return fallback;
7
+ }
8
+ const config = ns.get('config');
9
+ return config?.[key] != null ? config[key] : fallback;
10
+ }
11
+
12
+ ns.storage.IndexedDB = function indexedDBFactory() {
13
+ const dbName = configValue('indexedDBName', 'adaptfully');
14
+ const storeName = configValue('indexedDBStoreName', 'storage');
15
+ /** @type {Promise<IDBDatabase> | null} */
16
+ let dbPromise = null;
17
+
18
+ function openDb() {
19
+ if (!dbPromise) {
20
+ dbPromise = new Promise((resolve, reject) => {
21
+ const request = indexedDB.open(dbName, 1);
22
+ request.onupgradeneeded = () => {
23
+ request.result.createObjectStore(storeName);
24
+ };
25
+ request.onsuccess = () => resolve(request.result);
26
+ request.onerror = () => reject(request.error);
27
+ });
28
+ }
29
+ return dbPromise;
30
+ }
31
+
32
+ function withStore(mode, fn) {
33
+ return openDb().then((db) => new Promise((resolve, reject) => {
34
+ const tx = db.transaction(storeName, mode);
35
+ const store = tx.objectStore(storeName);
36
+ const request = fn(store);
37
+ request.onsuccess = () => resolve(request.result);
38
+ request.onerror = () => reject(request.error);
39
+ }));
40
+ }
41
+
42
+ return {
43
+ name: 'indexedDB',
44
+ async get(field) {
45
+ try {
46
+ const value = await withStore('readonly', (store) => store.get(field));
47
+ return value === undefined ? false : value;
48
+ } catch {
49
+ return false;
50
+ }
51
+ },
52
+ async set(field, value) {
53
+ await this.remove(field);
54
+ try {
55
+ await withStore('readwrite', (store) => store.put(value, field));
56
+ } catch {
57
+ // ignore
58
+ }
59
+ },
60
+ async remove(field) {
61
+ try {
62
+ await withStore('readwrite', (store) => store.delete(field));
63
+ } catch {
64
+ // ignore
65
+ }
66
+ },
67
+ async getObject(field) {
68
+ const raw = await this.get(field);
69
+ if (!raw) {
70
+ return false;
71
+ }
72
+ try {
73
+ return JSON.parse(raw);
74
+ } catch {
75
+ return false;
76
+ }
77
+ },
78
+ async setObject(field, obj) {
79
+ try {
80
+ await this.set(field, JSON.stringify(obj));
81
+ } catch {
82
+ // ignore
83
+ }
84
+ },
85
+ };
86
+ };
87
+ }(window.adaptfully));
@@ -0,0 +1,50 @@
1
+ /* global localStorage, window */
2
+
3
+ (function registerLocalStorage(ns) {
4
+ ns.storage.LocalStorage = function localStorageFactory() {
5
+ return {
6
+ name: 'localStorage',
7
+ get(field) {
8
+ try {
9
+ const value = localStorage.getItem(field);
10
+ return value === null ? false : value;
11
+ } catch {
12
+ return false;
13
+ }
14
+ },
15
+ set(field, value) {
16
+ this.remove(field);
17
+ try {
18
+ localStorage.setItem(field, value);
19
+ } catch {
20
+ // quota or private browsing
21
+ }
22
+ },
23
+ remove(field) {
24
+ try {
25
+ localStorage.removeItem(field);
26
+ } catch {
27
+ // ignore
28
+ }
29
+ },
30
+ getObject(field) {
31
+ const raw = this.get(field);
32
+ if (!raw) {
33
+ return false;
34
+ }
35
+ try {
36
+ return JSON.parse(raw);
37
+ } catch {
38
+ return false;
39
+ }
40
+ },
41
+ setObject(field, obj) {
42
+ try {
43
+ this.set(field, JSON.stringify(obj));
44
+ } catch {
45
+ // ignore
46
+ }
47
+ },
48
+ };
49
+ };
50
+ }(window.adaptfully));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/adaptfully",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",