@makefully/adaptfully 3.9.0 → 3.11.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,30 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 3.11.0 — 2026-07-23
6
+
7
+ ### Added
8
+
9
+ - **Layered icon validation** — build/deploy/release zips always include `meta/icon-foreground.png` and `meta/icon-background.png`. When project icons are missing under `assets/meta/`, Adaptfully **warns** (does not fail) and ships packaged placeholder icons from `lib/assets/`.
10
+ - **`resolveMetaIcons` / `appendMetaIcons`** — exported helpers for resolving project vs placeholder icon paths.
11
+
12
+ ### Changed
13
+
14
+ - Legacy meta packaging skips re-adding icon filenames so placeholders and project icons are not duplicated in the zip.
15
+
16
+ ## 3.10.0 — 2026-07-17
17
+
18
+ ### Added
19
+
20
+ - **`social-auth`** — Capgo `@capgo/capacitor-social-login` auth plugin for Capacitor platforms. Requires `packager: "capacitor"` and `platforms.<name>.socialLogin` provider config. Prebuild writes `social-login-config.js`; Wrapfully installs the native plugin when `socialLogin` is set on the build spec.
21
+ - **`CapacitorPackager`** CSP/viewport HTML extras and social-auth validation/templates.
22
+
23
+ ### Changed
24
+
25
+ - **Builder family defaults** — `android` / `ios` / `ios-sim` (and `-dev`) resolve to Wrapfully family **`capacitor`**. Explicit `packager: "cordova"` still selects Cordova for that platform. Composite `cordova` / `apple` routes are not used.
26
+ - **`resolveBuildSpec`** includes `socialLogin: boolean`.
27
+ - **Single-target Wrapfully POSTs** go to the target route (`/android/build`, `/ios/release`, …) instead of the family route (`/capacitor/...`), so OS relay and existing target routes keep working.
28
+
5
29
  ## 3.9.0 — 2026-07-15
6
30
 
7
31
  ### Added
package/README.md CHANGED
@@ -91,7 +91,8 @@ storage.getObject('currentGame');
91
91
  |------------|--------------|---------|
92
92
  | `google-auth` | `adaptfully.register('auth', adaptfully.auth.Google)` | Web, Android, iOS |
93
93
  | `steam-auth` | `adaptfully.register('auth', adaptfully.auth.Steam)` | Steam / Electron (via [steamworks.js](https://github.com/ceifa/steamworks.js)) |
94
- | `dev-auth` | `adaptfully.register('auth', adaptfully.auth.Dev)` | Local dev (test user) |
94
+ | `social-auth` | `adaptfully.register('auth', adaptfully.auth.Social)` | Capacitor (via [@capgo/capacitor-social-login](https://github.com/Cap-go/capacitor-social-login)) |
95
+ | `dev-auth` | `adaptfully.register('auth', adaptfully.auth.Dev)` | Local / testing |
95
96
 
96
97
  Use plugin keys in `config.platforms.<platform>.registrations`. Custom deploy scripts use a path relative to the deploy folder instead:
97
98
 
@@ -133,10 +134,14 @@ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `
133
134
  | `web` | `game-config.js` for **`uwp`** platform prebuilds |
134
135
  | `electron` | `main.js` (Electron shell); `preload.js` when **`steam-auth`** is registered |
135
136
  | `cordova` | `cordova.js` stub, `game-config.js`, CSP/viewport HTML extras |
136
- | `capacitor` | `game-config.js` (more Capacitor-specific output planned) |
137
+ | `capacitor` | `game-config.js`, Capacitor CSP/viewport; `social-login-config.js` when **`social-auth`** is registered |
138
+
139
+ Android/iOS Wrapfully builds default to the **`capacitor`** builder family. Set `packager: "cordova"` on a platform to use Cordova for that target.
137
140
 
138
141
  **`steam-auth` requires `packager: "electron"`** and `platforms.<name>.steamId` (Steamworks app ID for the client preload). Steam **upload** app IDs live separately in each Steam deployment’s `manifest.json` — see Wrapfully docs.
139
142
 
143
+ **`social-auth` requires `packager: "capacitor"`** and `platforms.<name>.socialLogin` (Capgo provider client IDs). Wrapfully installs `@capgo/capacitor-social-login` when building.
144
+
140
145
  ```json
141
146
  {
142
147
  "config": {
@@ -149,6 +154,20 @@ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `
149
154
  "packager": "electron",
150
155
  "steamId": 1234567,
151
156
  "registrations": { "auth": "steam-auth" }
157
+ },
158
+ "android": {
159
+ "packager": "capacitor",
160
+ "registrations": { "auth": "social-auth", "storage": "localStorage" },
161
+ "socialLogin": {
162
+ "providers": { "google": true, "apple": true },
163
+ "google": {
164
+ "webClientId": "….apps.googleusercontent.com",
165
+ "iOSClientId": "….apps.googleusercontent.com"
166
+ },
167
+ "apple": {
168
+ "clientId": "com.example.service"
169
+ }
170
+ }
152
171
  }
153
172
  }
154
173
  }
@@ -169,6 +188,17 @@ The renderer `steam-auth` plugin reads the Steam ID from that bridge. When Steam
169
188
  | `autoLoginStorageKey` | `lastLoggedIn` | Storage key written with the Steam ID on login |
170
189
  | `steamReadyTimeoutMs` | `10000` | Max wait when a bridge exists but identity is not yet ready |
171
190
 
191
+ #### Social auth (`social-auth`)
192
+
193
+ When `social-auth` is registered on a **`capacitor`** platform, Adaptfully prebuild writes **`social-login-config.js`** (`window.__ADAPTFULLY_SOCIAL_LOGIN__`) from `platforms.<name>.socialLogin`. Wrapfully installs [`@capgo/capacitor-social-login`](https://github.com/Cap-go/capacitor-social-login) and syncs it into the native project (same role as `steamworks.js` for Electron).
194
+
195
+ The runtime plugin calls Capgo `SocialLogin.initialize` / `login` / `logout` / `isLoggedIn`. Default provider is `google` on Android and `apple` on iOS when both are enabled (override with `socialLogin.defaultProvider`). Optional config keys:
196
+
197
+ | Key | Default | Purpose |
198
+ |-----|---------|---------|
199
+ | `autoLoginStorageKey` | `lastLoggedIn` | Storage key written with the user id on login |
200
+ | `socialReadyTimeoutMs` | `10000` | Max wait for the Capgo plugin bridge |
201
+
172
202
  ### Node API
173
203
 
174
204
  ```javascript
@@ -211,7 +241,7 @@ After prebuild, the build and deploy stages zip `output/<platform>-prebuild/` an
211
241
 
212
242
  ```bash
213
243
  npx adaptfully prebuild web
214
- npx adaptfully deploy steam http://build.example.com:9630/
244
+ npx adaptfully deploy steam http://build.example.com:9633/
215
245
  ```
216
246
 
217
247
  For web-only hosting (no Wrapfully), stop after prebuild and upload `output/web-prebuild/` yourself.
@@ -246,10 +276,10 @@ Examples:
246
276
  npx adaptfully prebuild web
247
277
 
248
278
  # Build for Steam via Wrapfully
249
- npx adaptfully build steam http://build.example.com:9630/
279
+ npx adaptfully build steam http://build.example.com:9633/
250
280
 
251
281
  # Full Steam deploy (build + upload when steam.json credentials are present)
252
- npx adaptfully deploy steam http://build.example.com:9630/
282
+ npx adaptfully deploy steam http://build.example.com:9633/
253
283
  ```
254
284
 
255
285
  Add scripts to your project's `package.json`:
@@ -278,7 +308,7 @@ The server URL is resolved in this order:
278
308
  1. CLI argument
279
309
  2. `WRAPFULLY_SERVER` environment variable
280
310
  3. `server` field in `wrapfully.json`
281
- 4. `http://localhost:9630/`
311
+ 4. `http://localhost:9633/`
282
312
 
283
313
  Keep server addresses and credentials out of version control — use environment variables or a gitignored `wrapfully.json`.
284
314
 
@@ -293,7 +323,7 @@ The client POSTs a zip stream built from `output/<platform>-prebuild/` to:
293
323
  For example, a project named `mygame` at version `1.2.0` with builder `android`:
294
324
 
295
325
  ```
296
- http://build.example.com:9630/android/mygame-1.2.0
326
+ http://build.example.com:9633/android/mygame-1.2.0
297
327
  ```
298
328
 
299
329
  The server extracts the zip, reads the embedded `package.json`, runs the build for that platform, and streams a zip of artifacts back to the client.
@@ -333,7 +363,7 @@ mygame/
333
363
 
334
364
  The selected deployment's folder is shipped as `meta/publish/` in the zip, so per-platform credential file names (`build.json`, `sftp.json`, `steam.json`, `apple.json`, `google.json`, `ms.json`, `android/`, `ms/`) live inside `assets/meta/deployments/<key>/`.
335
365
 
336
- Icons (`icon-foreground.png`, `icon-background.png`) are required for mobile, desktop, and Steam builds.
366
+ Icons (`icon-foreground.png`, `icon-background.png`) are required for mobile, desktop, and Steam builds. If either file is missing from `assets/meta/`, Adaptfully **warns** and ships packaged placeholder icons for that build so Wrapfully can still run.
337
367
 
338
368
  ### Icons
339
369
 
@@ -348,6 +378,8 @@ The build server composites the foreground over the background, applies a bindin
348
378
 
349
379
  **Recommended format:** 1536×1536 pixel square PNGs for both files. Images with other dimensions are scaled to 1536×1536 automatically, but matching the target size produces the sharpest results.
350
380
 
381
+ Missing icons do not fail the Adaptfully client: check the `WARNING missing layered icon(s)` log line and replace the placeholders before a store release.
382
+
351
383
  ## Configuration
352
384
 
353
385
  Build settings are read from `package.json`. The client merges any `wrapfully.json` fields into `package.json`'s `config` object before sending.
@@ -415,6 +447,7 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
415
447
  | `platforms.<name>.builders` | wrapfully-deploy | Map additional Wrapfully builder names to a platform |
416
448
  | `platforms.<name>.packager` | Prebuild | `web` (default), `electron`, `cordova`, or `capacitor` — selects the packager class that adds platform-specific files during prebuild |
417
449
  | `platforms.<name>.steamId` | Electron + steam-auth | Steamworks app ID baked into `preload.js` (client). Upload app IDs belong in the Steam deployment `manifest.json` |
450
+ | `platforms.<name>.socialLogin` | Capacitor + social-auth | Capgo provider config (`providers`, `google.webClientId`, `apple.clientId`, optional `defaultProvider`) |
418
451
  | `properties` | Cordova | Cordova config.xml entries (plugins, allow-navigation, etc.) |
419
452
 
420
453
  ### `wrapfully.json`
@@ -424,7 +457,7 @@ Optional. Fields are shallow-merged into `package.json`'s `config`:
424
457
  ```json
425
458
  {
426
459
  "deployFolder": "dist",
427
- "server": "http://build.example.com:9630/",
460
+ "server": "http://build.example.com:9633/",
428
461
  "title": "My Game",
429
462
  "packageName": "com.example.mygame"
430
463
  }
@@ -453,12 +486,8 @@ Each builder name becomes a path segment on the server. Some builds require a sp
453
486
  | `webapp` | Service-worker web app (optionally SFTP deploy) |
454
487
  | `steam` | Windows + Mac + Linux, uploads to Steam |
455
488
  | `steam-dev` | Debug Windows + Mac + Linux, no Steam upload |
456
- | `cordova` | Release Android + iOS |
457
- | `cordova-dev` | Debug Android + iOS |
458
- | `apple` | Release Mac + iOS |
459
- | `apple-dev` | Release Mac + debug iOS |
460
489
 
461
- For a single platform, pass the specific builder name rather than a composite.
490
+ For a single platform, pass the specific builder name.
462
491
 
463
492
  ### Platform package requirements
464
493
 
@@ -508,7 +537,7 @@ To deploy to Google Play, also include `assets/meta/deployments/<deployment>/goo
508
537
  }
509
538
  ```
510
539
 
511
- #### Apple (`ios`, `ios-dev`, `ios-sim`, `mac`, `apple`, `apple-dev`)
540
+ #### Apple (`ios`, `ios-dev`, `ios-sim`, `mac`)
512
541
 
513
542
  Include `assets/meta/deployments/<deployment>/build.json` with iOS signing settings:
514
543
 
@@ -544,9 +573,9 @@ To deploy to the App Store, include `assets/meta/deployments/<deployment>/apple.
544
573
  }
545
574
  ```
546
575
 
547
- #### Cordova (`cordova`, `cordova-dev`)
576
+ #### Cordova
548
577
 
549
- Requires the Android and Apple package requirements above.
578
+ Set `packager: "cordova"` on an `android` or `ios` platform to use Cordova instead of Capacitor. Requires the Android and Apple package requirements above.
550
579
 
551
580
  #### Steam (`steam`, `steam-dev`)
552
581
 
Binary file
Binary file
@@ -1,9 +1,9 @@
1
1
  import archiver from 'archiver';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { appendMetaIcons, isMetaIconFilename, META_DIR } from './icons.js';
4
5
 
5
6
  const
6
- META_DIR = 'assets/meta',
7
7
  DEPLOYMENTS_DIRNAME = 'deployments',
8
8
  PUBLISH_DIRNAME = 'publish';
9
9
 
@@ -38,6 +38,9 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
38
38
  log = options.log ?? (() => {}),
39
39
  metaDir = path.resolve(META_DIR);
40
40
 
41
+ // Always ship layered icons (project files or Adaptfully placeholders).
42
+ appendMetaIcons(zip, { metaDir, log });
43
+
41
44
  if (!fs.existsSync(metaDir)) {
42
45
  log(`adaptfully: meta dir missing at ${metaDir}; skipping credentials`);
43
46
  return;
@@ -46,7 +49,17 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
46
49
  const deploymentsRoot = path.join(metaDir, DEPLOYMENTS_DIRNAME);
47
50
 
48
51
  if (!fs.existsSync(deploymentsRoot)) {
49
- appendDirectory(zip, metaDir, 'meta');
52
+ for (const entry of fs.readdirSync(metaDir, { withFileTypes: true })) {
53
+ if (isMetaIconFilename(entry.name)) {
54
+ continue;
55
+ }
56
+ const fullPath = path.join(metaDir, entry.name);
57
+ if (entry.isDirectory()) {
58
+ appendDirectory(zip, fullPath, `meta/${entry.name}`);
59
+ } else {
60
+ zip.file(fullPath, { name: `meta/${entry.name}` });
61
+ }
62
+ }
50
63
  return;
51
64
  }
52
65
 
@@ -54,6 +67,9 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
54
67
  if (entry.name === DEPLOYMENTS_DIRNAME || entry.name === PUBLISH_DIRNAME) {
55
68
  continue;
56
69
  }
70
+ if (isMetaIconFilename(entry.name)) {
71
+ continue;
72
+ }
57
73
 
58
74
  const fullPath = path.join(metaDir, entry.name);
59
75
  if (entry.isDirectory()) {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
- const DEFAULT_SERVER = 'http://localhost:9630/';
3
+ const DEFAULT_SERVER = 'http://localhost:9633/';
4
4
 
5
5
  /**
6
6
  * @param {string} [projectRoot='.']
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getPackageRoot } from './paths.js';
4
+
5
+ export const META_DIR = 'assets/meta';
6
+ export const ICON_FOREGROUND = 'icon-foreground.png';
7
+ export const ICON_BACKGROUND = 'icon-background.png';
8
+
9
+ const ICON_FILES = [ICON_FOREGROUND, ICON_BACKGROUND];
10
+
11
+ /**
12
+ * Absolute path to a packaged Adaptfully placeholder icon.
13
+ * @param {string} filename
14
+ */
15
+ export function resolvePlaceholderIconPath(filename) {
16
+ return path.join(getPackageRoot(), 'lib', 'assets', filename);
17
+ }
18
+
19
+ /**
20
+ * @param {string} [metaDir=META_DIR]
21
+ */
22
+ export function resolveMetaIconPaths(metaDir = META_DIR) {
23
+ const root = path.resolve(metaDir);
24
+ return {
25
+ metaDir: root,
26
+ foreground: path.join(root, ICON_FOREGROUND),
27
+ background: path.join(root, ICON_BACKGROUND),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Resolve layered icon paths for Wrapfully packaging. Warns (does not throw) when
33
+ * project icons are missing and falls back to Adaptfully placeholders.
34
+ *
35
+ * @param {{ metaDir?: string, log?: (message: string) => void }} [options]
36
+ * @returns {{
37
+ * metaDir: string,
38
+ * foreground: string,
39
+ * background: string,
40
+ * missing: string[],
41
+ * usedPlaceholders: boolean,
42
+ * }}
43
+ */
44
+ export function resolveMetaIcons(options = {}) {
45
+ const log = options.log ?? console.log;
46
+ const { metaDir, foreground, background } = resolveMetaIconPaths(options.metaDir);
47
+ /** @type {string[]} */
48
+ const missing = [];
49
+ let resolvedForeground = foreground;
50
+ let resolvedBackground = background;
51
+
52
+ if (!fs.existsSync(foreground)) {
53
+ missing.push(ICON_FOREGROUND);
54
+ resolvedForeground = resolvePlaceholderIconPath(ICON_FOREGROUND);
55
+ }
56
+ if (!fs.existsSync(background)) {
57
+ missing.push(ICON_BACKGROUND);
58
+ resolvedBackground = resolvePlaceholderIconPath(ICON_BACKGROUND);
59
+ }
60
+
61
+ if (missing.length > 0) {
62
+ log(
63
+ `adaptfully: WARNING missing layered icon(s) in ${metaDir}: ${missing.join(', ')}. `
64
+ + 'Using Adaptfully placeholder icons for this build.',
65
+ );
66
+ }
67
+
68
+ return {
69
+ metaDir,
70
+ foreground: resolvedForeground,
71
+ background: resolvedBackground,
72
+ missing,
73
+ usedPlaceholders: missing.length > 0,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Append layered icons to a zip under meta/. Uses project icons when present;
79
+ * otherwise warns and ships Adaptfully placeholders.
80
+ *
81
+ * @param {import('archiver').Archiver} zip
82
+ * @param {{ metaDir?: string, log?: (message: string) => void }} [options]
83
+ * @returns {ReturnType<typeof resolveMetaIcons>}
84
+ */
85
+ export function appendMetaIcons(zip, options = {}) {
86
+ const resolved = resolveMetaIcons(options);
87
+ zip.file(resolved.foreground, { name: `meta/${ICON_FOREGROUND}` });
88
+ zip.file(resolved.background, { name: `meta/${ICON_BACKGROUND}` });
89
+ return resolved;
90
+ }
91
+
92
+ /**
93
+ * @param {string} name
94
+ */
95
+ export function isMetaIconFilename(name) {
96
+ return ICON_FILES.includes(name);
97
+ }
package/lib/node/index.js CHANGED
@@ -2,7 +2,17 @@ export { buildOutputDir, clearStaleBuildExtract, resolveBuildArtifactDir } from
2
2
  export { createArchive, createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
3
3
  export { loadProjectConfig, resolveServerUrl } from './config.js';
4
4
  export { send } from './deploy.js';
5
- export { adaptfullyFromCli, parseStageArgs, runAdaptfullyStage } from './pipeline.js';
5
+ export {
6
+ appendMetaIcons,
7
+ ICON_BACKGROUND,
8
+ ICON_FOREGROUND,
9
+ isMetaIconFilename,
10
+ META_DIR,
11
+ resolveMetaIconPaths,
12
+ resolveMetaIcons,
13
+ resolvePlaceholderIconPath,
14
+ } from './icons.js';
15
+ export { adaptfullyFromCli, parseStageArgs, resolveWrapfullyRoute, runAdaptfullyStage } from './pipeline.js';
6
16
  export { prebuildPlatform, prebuildOutputDir, resolveHtmlInjections } from './prebuild.js';
7
17
  export { getPackageRoot, getRuntimeDir, resolveRuntimeScript } from './paths.js';
8
18
  export {
@@ -223,6 +223,14 @@ export class Packager {
223
223
  + `Set config.platforms.${platformLabel}.packager to "electron".`,
224
224
  );
225
225
  }
226
+
227
+ if (this.usesPlugin('social-auth', { scope: 'all' }) && this.name !== 'capacitor') {
228
+ const platformLabel = this.platformKey ?? this.platforms[0] ?? 'unknown';
229
+ throw new Error(
230
+ `Platform "${platformLabel}" uses social-auth but packager is "${this.name}". `
231
+ + `Set config.platforms.${platformLabel}.packager to "capacitor".`,
232
+ );
233
+ }
226
234
  }
227
235
 
228
236
  needsGameConfig() {
@@ -1,5 +1,81 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { Packager } from './base.js';
2
4
 
5
+ const CAPACITOR_CSP = '<meta http-equiv="Content-Security-Policy" content="default-src * \'self\' data: blob: \'unsafe-inline\' \'unsafe-eval\'; style-src * \'self\' \'unsafe-inline\'; script-src * \'self\' \'unsafe-inline\' \'unsafe-eval\'; connect-src *; img-src * data: blob:; media-src * data: blob:; frame-src *;" />';
6
+
7
+ const CAPACITOR_VIEWPORT = '<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />';
8
+
9
+ /**
10
+ * @param {string | null | undefined} platformKey
11
+ * @param {{ config?: { platforms?: Record<string, { socialLogin?: object }> } }} pkg
12
+ */
13
+ export function resolvePlatformSocialLogin(platformKey, pkg) {
14
+ if (!platformKey) {
15
+ return undefined;
16
+ }
17
+ return pkg.config?.platforms?.[platformKey]?.socialLogin;
18
+ }
19
+
20
+ /**
21
+ * @param {object} socialLogin
22
+ * @param {string} platformKey
23
+ */
24
+ function normalizeSocialLoginConfig(socialLogin = {}, platformKey) {
25
+ const providers = socialLogin.providers ?? { google: true, apple: true };
26
+ const googleEnabled = providers.google !== false;
27
+ const appleEnabled = providers.apple !== false;
28
+
29
+ let defaultProvider = socialLogin.defaultProvider;
30
+ if (!defaultProvider) {
31
+ if (platformKey === 'ios' || platformKey === 'ios-dev' || platformKey === 'ios-sim') {
32
+ defaultProvider = appleEnabled ? 'apple' : 'google';
33
+ } else {
34
+ defaultProvider = googleEnabled ? 'google' : 'apple';
35
+ }
36
+ }
37
+
38
+ return {
39
+ providers: {
40
+ google: googleEnabled,
41
+ apple: appleEnabled,
42
+ facebook: providers.facebook === true,
43
+ twitter: providers.twitter === true,
44
+ },
45
+ google: socialLogin.google ?? {},
46
+ apple: socialLogin.apple ?? {},
47
+ defaultProvider,
48
+ platform: platformKey,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {object} config
54
+ */
55
+ function validateSocialLoginConfig(config) {
56
+ const { providers, google, apple } = config;
57
+
58
+ if (!providers.google && !providers.apple) {
59
+ throw new Error('social-auth requires at least one of socialLogin.providers.google or .apple');
60
+ }
61
+
62
+ if (providers.google) {
63
+ if (!google?.webClientId) {
64
+ throw new Error(
65
+ 'social-auth with Google requires platforms.<key>.socialLogin.google.webClientId',
66
+ );
67
+ }
68
+ }
69
+
70
+ if (providers.apple && config.platform?.startsWith('android')) {
71
+ if (!apple?.clientId) {
72
+ throw new Error(
73
+ 'social-auth with Apple on Android requires platforms.<key>.socialLogin.apple.clientId',
74
+ );
75
+ }
76
+ }
77
+ }
78
+
3
79
  export class CapacitorPackager extends Packager {
4
80
  /** @type {'capacitor'} */
5
81
  static id = 'capacitor';
@@ -7,7 +83,57 @@ export class CapacitorPackager extends Packager {
7
83
  /** @type {string[]} */
8
84
  static defaultPlatforms = ['ios', 'android'];
9
85
 
86
+ validate() {
87
+ super.validate();
88
+
89
+ if (this.usesPlugin('social-auth')) {
90
+ const socialLogin = resolvePlatformSocialLogin(this.platformKey, this.pkg);
91
+ if (!socialLogin && !this.pkg.config?.socialLogin) {
92
+ const platformLabel = this.platformKey ?? this.platforms.join(', ');
93
+ throw new Error(
94
+ `Platform "${platformLabel}" uses social-auth but platforms.${platformLabel}.socialLogin is not set.`,
95
+ );
96
+ }
97
+
98
+ const config = normalizeSocialLoginConfig(
99
+ socialLogin ?? this.pkg.config?.socialLogin,
100
+ this.platformKey ?? 'android',
101
+ );
102
+ validateSocialLoginConfig(config);
103
+ }
104
+ }
105
+
10
106
  needsGameConfig() {
11
107
  return true;
12
108
  }
109
+
110
+ /** @param {string} dest */
111
+ applyTemplates(dest) {
112
+ if (this.usesPlugin('social-auth') && this.platformKey) {
113
+ const socialLogin = resolvePlatformSocialLogin(this.platformKey, this.pkg)
114
+ ?? this.pkg.config?.socialLogin
115
+ ?? {};
116
+ const config = normalizeSocialLoginConfig(socialLogin, this.platformKey);
117
+ const content = `window.__ADAPTFULLY_SOCIAL_LOGIN__ = ${JSON.stringify(config, null, 4)};\n`;
118
+ fs.writeFileSync(path.join(dest, 'social-login-config.js'), content);
119
+ this.log('adaptfully: write social-login-config.js (social-auth)');
120
+ }
121
+
122
+ super.applyTemplates(dest);
123
+ }
124
+
125
+ buildHtmlInjection() {
126
+ const headExtras = [CAPACITOR_CSP, CAPACITOR_VIEWPORT];
127
+ const bodyScripts = [];
128
+
129
+ if (this.usesPlugin('social-auth')) {
130
+ bodyScripts.push('<script src="social-login-config.js"></script>');
131
+ }
132
+
133
+ if (this.needsGameConfig()) {
134
+ bodyScripts.push('<script src="game-config.js"></script>');
135
+ }
136
+
137
+ return this.formatHtmlInjection(headExtras, bodyScripts);
138
+ }
13
139
  }
@@ -1,19 +1,19 @@
1
1
  import { CapacitorPackager } from './capacitor.js';
2
2
  import { CordovaPackager } from './cordova.js';
3
- import { ElectronPackager, buildElectronMain, buildElectronPreload } from './electron.js';
3
+ import { ElectronPackager } from './electron.js';
4
4
  import { Packager, VALID_PACKAGERS } from './base.js';
5
5
  import { WebPackager } from './web.js';
6
6
 
7
- export { Packager, VALID_PACKAGERS } from './base.js';
8
- export { WebPackager } from './web.js';
7
+ export { CapacitorPackager, resolvePlatformSocialLogin } from './capacitor.js';
8
+ export { CordovaPackager } from './cordova.js';
9
9
  export {
10
10
  ElectronPackager,
11
11
  buildElectronMain,
12
12
  buildElectronPreload,
13
13
  resolvePlatformSteamId,
14
14
  } from './electron.js';
15
- export { CordovaPackager } from './cordova.js';
16
- export { CapacitorPackager } from './capacitor.js';
15
+ export { Packager, VALID_PACKAGERS } from './base.js';
16
+ export { WebPackager } from './web.js';
17
17
 
18
18
  /** @typedef {import('./base.js').PackagerName} PackagerName */
19
19
 
@@ -72,6 +72,13 @@ export function usesSteamAuth(registrations) {
72
72
  return Object.values(registrations).includes('steam-auth');
73
73
  }
74
74
 
75
+ /**
76
+ * @param {Record<string, string>} registrations
77
+ */
78
+ export function usesSocialAuth(registrations) {
79
+ return Object.values(registrations).includes('social-auth');
80
+ }
81
+
75
82
  /**
76
83
  * @param {string} dest
77
84
  * @param {string} platformKey
@@ -14,6 +14,21 @@ import { steamAuthFromCli } from './steam-auth.js';
14
14
 
15
15
  const VALID_STAGES = new Set(['prebuild', 'build', 'deploy', 'release']);
16
16
 
17
+ /**
18
+ * Wrapfully URL path segment for build/release.
19
+ * Single-target platforms post to the target route (`android`, `ios`, …) so OS relay
20
+ * and long-standing target routes work; multi-target platforms post to the family
21
+ * (`electron`, …).
22
+ *
23
+ * @param {{ family: string, targets: string[] }} buildSpec
24
+ */
25
+ export function resolveWrapfullyRoute(buildSpec) {
26
+ if (buildSpec.targets.length === 1) {
27
+ return buildSpec.targets[0];
28
+ }
29
+ return buildSpec.family;
30
+ }
31
+
17
32
  /**
18
33
  * @param {AdaptfullyStage} stage
19
34
  * @param {string} platformKey
@@ -69,10 +84,15 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
69
84
  /** @type {{ deploymentKey: string, stage: string, family: string }[]} */
70
85
  const sent = [];
71
86
 
87
+ const wrapfullyRoute = resolveWrapfullyRoute(buildSpec);
88
+
72
89
  if (stage === 'release') {
73
90
  const deploymentDirs = deployments.map((key) => resolvePublishDir(key));
74
91
 
75
- log(`adaptfully: ${stage} ${platformKey} → ${buildSpec.family} (${buildSpec.targets.join(', ')})`);
92
+ log(
93
+ `adaptfully: ${stage} ${platformKey} → ${buildSpec.family} `
94
+ + `(${buildSpec.targets.join(', ')}) via /${wrapfullyRoute}/release`,
95
+ );
76
96
 
77
97
  log(
78
98
  `adaptfully: release deployments → ${
@@ -85,7 +105,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
85
105
  contents,
86
106
  options.server,
87
107
  'release',
88
- buildSpec.family,
108
+ wrapfullyRoute,
89
109
  prebuiltDir,
90
110
  pkg,
91
111
  options.mode ?? 'extract',
@@ -107,7 +127,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
107
127
  log(
108
128
  `adaptfully: ${stage} ${platformKey} → `
109
129
  + `${deploymentKey === 'zip' ? 'artifact zip' : `deployment "${deploymentKey}"`} `
110
- + `via Wrapfully ${buildSpec.family}`,
130
+ + `via Wrapfully /${wrapfullyRoute}/${wrapStage} (${buildSpec.family})`,
111
131
  );
112
132
 
113
133
  await send(
@@ -115,7 +135,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
115
135
  contents,
116
136
  options.server,
117
137
  wrapStage,
118
- buildSpec.family,
138
+ wrapfullyRoute,
119
139
  prebuiltDir,
120
140
  pkg,
121
141
  options.mode ?? 'extract',
@@ -158,9 +178,9 @@ export async function adaptfullyFromCli(argv = process.argv) {
158
178
  + '\n'
159
179
  + 'Stages:\n'
160
180
  + ' prebuild Copy deploy/ and apply platform registrations → output/<platform>-prebuild/\n'
161
- + ' build prebuild + POST /{family}/build (zip artifact only)\n'
181
+ + ' build prebuild + POST /{target|family}/build (zip artifact only)\n'
162
182
  + ' deploy POST /deploy/{key} with prior artifact + deployment credentials\n'
163
- + ' release prebuild + POST /{family}/release (build + configured deployments)\n'
183
+ + ' release prebuild + POST /{target|family}/release (build + configured deployments)\n'
164
184
  + '\n'
165
185
  + ' --deployment <key> Target a single named deployment.\n'
166
186
  + ' --artifact <path> Prior build artifact directory (deploy only; default: ./output/ after build).\n'
@@ -22,6 +22,10 @@ export const STANDARD_PLUGINS = {
22
22
  scripts: ['core.js', 'platform.js', 'auth/_helpers.js', 'auth/steam-auth.js'],
23
23
  registration: (key) => `adaptfully.register('${key}', adaptfully.auth.Steam);`,
24
24
  },
25
+ 'social-auth': {
26
+ scripts: ['core.js', 'platform.js', 'auth/_helpers.js', 'auth/social-auth.js'],
27
+ registration: (key) => `adaptfully.register('${key}', adaptfully.auth.Social);`,
28
+ },
25
29
  'dev-auth': {
26
30
  scripts: ['core.js', 'platform.js', 'auth/_helpers.js', 'auth/dev-auth.js'],
27
31
  registration: (key) => `adaptfully.register('${key}', adaptfully.auth.Dev);`,
@@ -52,10 +56,6 @@ export const DEFAULT_BUILDER_PLATFORMS = {
52
56
  'ios-dev': 'ios',
53
57
  'ios-sim': 'ios',
54
58
  webapp: 'web',
55
- cordova: 'cordova',
56
- 'cordova-dev': 'cordova',
57
- apple: 'apple',
58
- 'apple-dev': 'apple',
59
59
  uwp: 'uwp',
60
60
  };
61
61
 
@@ -340,7 +340,7 @@ export function resolvePublishDir(deploymentKey, metaDir = 'assets/meta') {
340
340
  const ELECTRON_TARGETS = new Set([
341
341
  'win', 'win-dev', 'mac', 'mac-dev', 'linux', 'linux-dev',
342
342
  ]);
343
- const CORDOVA_TARGETS = new Set([
343
+ const MOBILE_TARGETS = new Set([
344
344
  'android', 'android-dev', 'ios', 'ios-dev', 'ios-sim',
345
345
  ]);
346
346
 
@@ -352,8 +352,8 @@ export function resolveFamilyFromTarget(target) {
352
352
  if (ELECTRON_TARGETS.has(target)) {
353
353
  return 'electron';
354
354
  }
355
- if (CORDOVA_TARGETS.has(target)) {
356
- return 'cordova';
355
+ if (MOBILE_TARGETS.has(target)) {
356
+ return 'capacitor';
357
357
  }
358
358
  if (target === 'pwa') {
359
359
  return 'pwa';
@@ -379,9 +379,12 @@ export function resolveFamily(targets, packager) {
379
379
  if (packager === 'electron') {
380
380
  return 'electron';
381
381
  }
382
- if (packager === 'cordova' || packager === 'capacitor') {
382
+ if (packager === 'cordova') {
383
383
  return 'cordova';
384
384
  }
385
+ if (packager === 'capacitor') {
386
+ return 'capacitor';
387
+ }
385
388
 
386
389
  const families = [...new Set(targets.map((target) => resolveFamilyFromTarget(target)).filter(Boolean))];
387
390
 
@@ -396,20 +399,35 @@ export function resolveFamily(targets, packager) {
396
399
  throw new Error(`Cannot resolve builder family for targets [${targets.join(', ')}]`);
397
400
  }
398
401
 
402
+ /**
403
+ * @param {object | undefined} platform
404
+ * @returns {boolean}
405
+ */
406
+ function platformUsesSocialAuth(platform) {
407
+ if (!platform?.registrations) {
408
+ return false;
409
+ }
410
+ return Object.values(platform.registrations).includes('social-auth');
411
+ }
412
+
399
413
  /**
400
414
  * @param {string} platformKey
401
- * @param {{ config?: { platforms?: Record<string, { builder?: string | string[], packager?: string, steamworks?: boolean, deployments?: string[] }> } }} pkg
415
+ * @param {{ config?: { platforms?: Record<string, { builder?: string | string[], packager?: string, steamworks?: boolean, socialLogin?: boolean | object, registrations?: Record<string, string>, deployments?: string[] }> } }} pkg
402
416
  */
403
417
  export function resolveBuildSpec(platformKey, pkg) {
404
418
  const platform = pkg.config?.platforms?.[platformKey];
405
419
  const builder = platform?.builder ?? (platformKey === 'web' ? 'webapp' : platformKey);
406
420
  const targets = Array.isArray(builder) ? [...builder] : [builder];
421
+ const socialLogin = platform?.socialLogin === true
422
+ || (typeof platform?.socialLogin === 'object' && platform.socialLogin != null)
423
+ || platformUsesSocialAuth(platform);
407
424
 
408
425
  return {
409
426
  family: resolveFamily(targets, platform?.packager),
410
427
  targets,
411
428
  platformKey,
412
429
  steamworks: platform?.steamworks === true,
430
+ socialLogin,
413
431
  deployments: resolveDeploymentsForPlatform(platformKey, pkg),
414
432
  };
415
433
  }
@@ -0,0 +1,313 @@
1
+ /* global window */
2
+
3
+ /**
4
+ * Social auth via @capgo/capacitor-social-login.
5
+ * Adaptfully prebuild writes social-login-config.js and Wrapfully installs the Capgo plugin
6
+ * when packager is "capacitor" and registrations.auth is "social-auth".
7
+ */
8
+ (function registerSocialAuth(ns) {
9
+ const { configValue, getStorage } = ns.auth.helpers;
10
+
11
+ const DEFAULT_AUTO_LOGIN_KEY = 'lastLoggedIn';
12
+ const READY_POLL_MS = 50;
13
+ const READY_TIMEOUT_MS = 10000;
14
+
15
+ function readSocialLoginConfig() {
16
+ return window.__ADAPTFULLY_SOCIAL_LOGIN__ || {};
17
+ }
18
+
19
+ function resolveSocialLoginPlugin() {
20
+ const plugins = window.Capacitor?.Plugins;
21
+ if (plugins?.SocialLogin) {
22
+ return plugins.SocialLogin;
23
+ }
24
+ if (window.SocialLogin) {
25
+ return window.SocialLogin;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function mapLoginResult(result) {
31
+ const resultResult = result?.result ?? result;
32
+ const profile = resultResult?.profile
33
+ || resultResult?.user
34
+ || resultResult
35
+ || {};
36
+ const id = String(
37
+ profile.id
38
+ || profile.user
39
+ || profile.sub
40
+ || resultResult?.accessToken?.userId
41
+ || resultResult?.idToken
42
+ || '',
43
+ );
44
+ const email = String(profile.email || '');
45
+
46
+ if (!id && !email) {
47
+ return null;
48
+ }
49
+
50
+ return {
51
+ id: id || email,
52
+ email,
53
+ displayName: profile.name || profile.givenName || '',
54
+ };
55
+ }
56
+
57
+ class SocialAuthPlugin {
58
+ constructor() {
59
+ this.name = 'social';
60
+ this.user = null;
61
+ this.authenticated = false;
62
+ this.online = false;
63
+ this.#plugin = null;
64
+ this.#provider = null;
65
+ }
66
+
67
+ /** @type {object | null} */
68
+ #plugin;
69
+
70
+ /** @type {string | null} */
71
+ #provider;
72
+
73
+ supportsAutoLogin() {
74
+ return true;
75
+ }
76
+
77
+ #autoLoginStorageKey() {
78
+ return configValue('autoLoginStorageKey', DEFAULT_AUTO_LOGIN_KEY);
79
+ }
80
+
81
+ #config() {
82
+ return readSocialLoginConfig();
83
+ }
84
+
85
+ #resolveDefaultProvider() {
86
+ const config = this.#config();
87
+ if (config.defaultProvider) {
88
+ return config.defaultProvider;
89
+ }
90
+ const platform = config.platform || window.gameConfig?.platform || '';
91
+ if (String(platform).startsWith('ios')) {
92
+ return config.providers?.apple === false ? 'google' : 'apple';
93
+ }
94
+ return config.providers?.google === false ? 'apple' : 'google';
95
+ }
96
+
97
+ #buildInitializeOptions() {
98
+ const config = this.#config();
99
+ const options = {};
100
+
101
+ if (config.providers?.google !== false) {
102
+ options.google = {
103
+ webClientId: config.google?.webClientId
104
+ || configValue('googleClientId', ''),
105
+ iOSClientId: config.google?.iOSClientId,
106
+ iOSServerClientId: config.google?.iOSServerClientId
107
+ || config.google?.webClientId,
108
+ mode: config.google?.mode || 'online',
109
+ };
110
+ }
111
+
112
+ if (config.providers?.apple !== false) {
113
+ options.apple = {
114
+ clientId: config.apple?.clientId || '',
115
+ redirectUrl: config.apple?.redirectUrl,
116
+ useProperTokenExchange: config.apple?.useProperTokenExchange !== false,
117
+ useBroadcastChannel: config.apple?.useBroadcastChannel !== false,
118
+ };
119
+ }
120
+
121
+ return options;
122
+ }
123
+
124
+ #persistLogin(user) {
125
+ const storage = getStorage();
126
+ storage?.set(this.#autoLoginStorageKey(), user.id);
127
+ }
128
+
129
+ #applyIdentity(identity) {
130
+ if (!identity?.id) {
131
+ this.user = null;
132
+ this.authenticated = false;
133
+ this.online = false;
134
+ return false;
135
+ }
136
+
137
+ this.user = {
138
+ id: identity.id,
139
+ email: identity.email || '',
140
+ displayName: identity.displayName || '',
141
+ };
142
+ this.authenticated = true;
143
+ this.online = true;
144
+ this.#persistLogin(this.user);
145
+ return true;
146
+ }
147
+
148
+ #complete(callback) {
149
+ callback({
150
+ authenticated: this.authenticated,
151
+ user: this.getUser(),
152
+ });
153
+ }
154
+
155
+ whenReady(done) {
156
+ const started = Date.now();
157
+ const timeoutMs = Number(configValue('socialReadyTimeoutMs', READY_TIMEOUT_MS))
158
+ || READY_TIMEOUT_MS;
159
+
160
+ const finish = async () => {
161
+ const plugin = resolveSocialLoginPlugin();
162
+ if (!plugin) {
163
+ if (Date.now() - started >= timeoutMs) {
164
+ console.warn('[adaptfully social-auth] Capgo SocialLogin plugin not available');
165
+ this.online = false;
166
+ done({ error: 'SocialLogin plugin not available' });
167
+ return;
168
+ }
169
+ window.setTimeout(finish, READY_POLL_MS);
170
+ return;
171
+ }
172
+
173
+ this.#plugin = plugin;
174
+ this.#provider = this.#resolveDefaultProvider();
175
+
176
+ try {
177
+ await plugin.initialize(this.#buildInitializeOptions());
178
+ this.online = true;
179
+ done();
180
+ } catch (err) {
181
+ console.error('[adaptfully social-auth] initialize failed:', err);
182
+ this.online = false;
183
+ done({ error: err?.message || 'SocialLogin initialize failed' });
184
+ }
185
+ };
186
+
187
+ finish();
188
+ }
189
+
190
+ login(callback) {
191
+ const plugin = this.#plugin ?? resolveSocialLoginPlugin();
192
+ const provider = this.#provider ?? this.#resolveDefaultProvider();
193
+
194
+ if (!plugin) {
195
+ console.warn('[adaptfully social-auth] login: plugin missing');
196
+ this.#complete(callback);
197
+ return;
198
+ }
199
+
200
+ plugin.login({
201
+ provider,
202
+ options: {
203
+ scopes: provider === 'google'
204
+ ? ['email', 'profile']
205
+ : ['email', 'name'],
206
+ },
207
+ })
208
+ .then((result) => {
209
+ if (!this.#applyIdentity(mapLoginResult(result))) {
210
+ console.warn('[adaptfully social-auth] login: could not map identity', result);
211
+ }
212
+ this.#complete(callback);
213
+ })
214
+ .catch((err) => {
215
+ console.error('[adaptfully social-auth] login failed:', err);
216
+ this.user = null;
217
+ this.authenticated = false;
218
+ this.online = false;
219
+ this.#complete(callback);
220
+ });
221
+ }
222
+
223
+ autoLogin(callback) {
224
+ const plugin = this.#plugin ?? resolveSocialLoginPlugin();
225
+ const provider = this.#provider ?? this.#resolveDefaultProvider();
226
+ const storage = getStorage();
227
+ const lastId = storage?.get?.(this.#autoLoginStorageKey());
228
+
229
+ if (!plugin || typeof plugin.isLoggedIn !== 'function') {
230
+ if (lastId) {
231
+ this.#applyIdentity({ id: String(lastId), email: '' });
232
+ }
233
+ this.#complete(callback);
234
+ return;
235
+ }
236
+
237
+ plugin.isLoggedIn({ provider })
238
+ .then(async (status) => {
239
+ if (!status?.isLoggedIn) {
240
+ this.user = null;
241
+ this.authenticated = false;
242
+ this.#complete(callback);
243
+ return;
244
+ }
245
+
246
+ if (typeof plugin.getAuthorizationCode === 'function') {
247
+ try {
248
+ const auth = await plugin.getAuthorizationCode({ provider });
249
+ const identity = mapLoginResult(auth) || (lastId
250
+ ? { id: String(lastId), email: '' }
251
+ : null);
252
+ this.#applyIdentity(identity);
253
+ this.#complete(callback);
254
+ return;
255
+ } catch {
256
+ // fall through to lastId
257
+ }
258
+ }
259
+
260
+ if (lastId) {
261
+ this.#applyIdentity({ id: String(lastId), email: '' });
262
+ }
263
+ this.#complete(callback);
264
+ })
265
+ .catch((err) => {
266
+ console.warn('[adaptfully social-auth] autoLogin failed:', err);
267
+ this.#complete(callback);
268
+ });
269
+ }
270
+
271
+ logout(callback) {
272
+ const plugin = this.#plugin ?? resolveSocialLoginPlugin();
273
+ const provider = this.#provider ?? this.#resolveDefaultProvider();
274
+ const storage = getStorage();
275
+
276
+ const clear = () => {
277
+ this.user = null;
278
+ this.authenticated = false;
279
+ this.online = false;
280
+ storage?.remove(this.#autoLoginStorageKey());
281
+ callback();
282
+ };
283
+
284
+ if (!plugin || typeof plugin.logout !== 'function') {
285
+ clear();
286
+ return;
287
+ }
288
+
289
+ plugin.logout({ provider })
290
+ .then(clear)
291
+ .catch((err) => {
292
+ console.warn('[adaptfully social-auth] logout failed:', err);
293
+ clear();
294
+ });
295
+ }
296
+
297
+ getUser() {
298
+ if (!this.authenticated || !this.user) {
299
+ return null;
300
+ }
301
+ return {
302
+ id: this.user.id,
303
+ email: this.user.email || '',
304
+ };
305
+ }
306
+
307
+ isAuthenticated() {
308
+ return !!this.authenticated;
309
+ }
310
+ }
311
+
312
+ ns.auth.Social = () => new SocialAuthPlugin();
313
+ }(window.adaptfully));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/adaptfully",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",