kasy-cli 1.21.7 → 1.21.9
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/lib/commands/new.js
CHANGED
|
@@ -44,7 +44,7 @@ const { generateApiProject } = require('../scaffold/backends/api/generator');
|
|
|
44
44
|
const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys } = require('../scaffold/backends/supabase/deploy');
|
|
45
45
|
const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
|
|
46
46
|
const { toPackageName } = require('../scaffold/backends/firebase/tokens');
|
|
47
|
-
const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
47
|
+
const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
48
48
|
const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
|
|
49
49
|
const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
|
|
50
50
|
|
|
@@ -1689,10 +1689,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1689
1689
|
// client, which is why Google used to come out disabled on Supabase projects.
|
|
1690
1690
|
const googleSpinner = ui.spinner();
|
|
1691
1691
|
googleSpinner.start(tr('new.google.enabling'));
|
|
1692
|
+
// The deploy below needs the project's Identity Platform config to exist.
|
|
1693
|
+
// Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
|
|
1694
|
+
// Auth untouched, so initialize it here (idempotent) before the deploy.
|
|
1695
|
+
await ensureFirebaseAuthInitialized(answers.firebaseProjectId);
|
|
1692
1696
|
const cliResult = await enableAuthViaFirebaseCli({
|
|
1693
1697
|
projectDir: targetDir,
|
|
1694
1698
|
projectId: answers.firebaseProjectId,
|
|
1695
1699
|
appName: answers.appName,
|
|
1700
|
+
googleOnly: true,
|
|
1696
1701
|
});
|
|
1697
1702
|
googleSpinner.stop(tr('new.google.enabling'));
|
|
1698
1703
|
|
|
@@ -63,9 +63,12 @@ async function mergeAuthIntoFirebaseJson(projectDir, providers) {
|
|
|
63
63
|
* @param {string} options.projectId
|
|
64
64
|
* @param {string} options.appName
|
|
65
65
|
* @param {string} [options.supportEmail] - falls back to active gcloud account
|
|
66
|
+
* @param {boolean} [options.googleOnly] - Supabase: enable ONLY Google (the deploy's
|
|
67
|
+
* side effect creates the OAuth Web Client we need). Anonymous/Email/Password in
|
|
68
|
+
* Firebase Auth would be dead config there, since the app uses Supabase Auth.
|
|
66
69
|
* @returns {{ ok: boolean, error?: string, supportEmail?: string }}
|
|
67
70
|
*/
|
|
68
|
-
async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, supportEmail }) {
|
|
71
|
+
async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, supportEmail, googleOnly = false }) {
|
|
69
72
|
// 1. Resolve support email (required by Google's OAuth consent screen)
|
|
70
73
|
let email = (supportEmail || '').trim();
|
|
71
74
|
if (!email) email = await getGcloudAccountEmail();
|
|
@@ -76,15 +79,20 @@ async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, suppor
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
// 2. Merge firebase.json
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// 2. Merge firebase.json. Google is always enabled (it's the whole point — the
|
|
83
|
+
// deploy creates its OAuth Web Client). Anonymous + Email/Password are added only
|
|
84
|
+
// for the Firebase backend, whose app actually signs in through Firebase Auth.
|
|
85
|
+
const providers = {
|
|
83
86
|
googleSignIn: {
|
|
84
87
|
oAuthBrandDisplayName: appName,
|
|
85
88
|
supportEmail: email,
|
|
86
89
|
},
|
|
87
|
-
}
|
|
90
|
+
};
|
|
91
|
+
if (!googleOnly) {
|
|
92
|
+
providers.anonymous = true;
|
|
93
|
+
providers.emailPassword = true;
|
|
94
|
+
}
|
|
95
|
+
const merge = await mergeAuthIntoFirebaseJson(projectDir, providers);
|
|
88
96
|
if (!merge.ok) return merge;
|
|
89
97
|
|
|
90
98
|
// 3. Deploy. --non-interactive prevents the CLI from prompting on edge cases.
|
|
@@ -19,6 +19,7 @@ const path = require('node:path');
|
|
|
19
19
|
const fs = require('fs-extra');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const { getInstallGuide } = require('../../../utils/checks');
|
|
22
|
+
const { keytoolBin } = require('../../../utils/env-tools');
|
|
22
23
|
|
|
23
24
|
const execAsync = promisify(exec);
|
|
24
25
|
|
|
@@ -536,7 +537,7 @@ async function ensureDebugKeystore() {
|
|
|
536
537
|
if (!(await fs.pathExists(DEBUG_KEYSTORE))) {
|
|
537
538
|
await fs.ensureDir(androidDir);
|
|
538
539
|
const createResult = await run(
|
|
539
|
-
|
|
540
|
+
`${keytoolBin()} -genkeypair -v -keystore "${DEBUG_KEYSTORE}" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US" -noprompt`
|
|
540
541
|
);
|
|
541
542
|
if (!createResult.ok) return { ok: false, error: createResult.stderr || createResult.error };
|
|
542
543
|
}
|
|
@@ -552,7 +553,7 @@ async function extractSha1() {
|
|
|
552
553
|
if (!ensureResult.ok) return ensureResult;
|
|
553
554
|
|
|
554
555
|
const result = await run(
|
|
555
|
-
|
|
556
|
+
`${keytoolBin()} -list -v -keystore "${DEBUG_KEYSTORE}" -alias androiddebugkey -storepass android -keypass android`
|
|
556
557
|
);
|
|
557
558
|
if (!result.ok) return { ok: false, error: result.stderr || result.error };
|
|
558
559
|
|
|
@@ -729,34 +730,24 @@ async function ensureLocalhostAuthorizedDomains(projectId, token) {
|
|
|
729
730
|
}
|
|
730
731
|
|
|
731
732
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
733
|
+
* Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
|
|
734
|
+
* have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
|
|
735
|
+
* CLI `deploy --only auth`) fails with CONFIGURATION_NOT_FOUND until this runs.
|
|
734
736
|
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* 2. PATCH /config to enable Email/Password and Anonymous.
|
|
739
|
-
* 3. POST defaultSupportedIdpConfigs to enable Google Sign-In.
|
|
740
|
-
* Google Sign-In requires an OAuth client_id that Firebase creates when the user
|
|
741
|
-
* enables it in the Console — if the client doesn't exist yet this step is skipped
|
|
742
|
-
* and googleSignInSkipped=true is returned so the caller can show a targeted hint.
|
|
743
|
-
*
|
|
744
|
-
* Non-fatal: returns { ok: false, error } without throwing if the API call fails.
|
|
737
|
+
* The endpoint is idempotent: it returns {} both on first init and when auth was
|
|
738
|
+
* already initialized. Non-fatal by contract — callers proceed on failure since
|
|
739
|
+
* the config may already exist.
|
|
745
740
|
*
|
|
746
741
|
* @param {string} projectId
|
|
747
|
-
* @returns {{ ok: boolean,
|
|
742
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
748
743
|
*/
|
|
749
|
-
async function
|
|
744
|
+
async function ensureFirebaseAuthInitialized(projectId, { maxRetries = 3, retryDelayMs = 15000 } = {}) {
|
|
750
745
|
let token;
|
|
751
746
|
try {
|
|
752
747
|
token = await getAccessToken();
|
|
753
748
|
} catch (_) {
|
|
754
749
|
return { ok: false, error: 'Could not get access token' };
|
|
755
750
|
}
|
|
756
|
-
|
|
757
|
-
// Step 1: Initialize Firebase Auth for the project.
|
|
758
|
-
// New projects have no auth config — PATCH returns CONFIGURATION_NOT_FOUND without this.
|
|
759
|
-
// The endpoint is idempotent: it returns {} on success or if already initialized.
|
|
760
751
|
const initUrl = `https://identitytoolkit.googleapis.com/v2/projects/${projectId}/identityPlatform:initializeAuth`;
|
|
761
752
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
762
753
|
const initRes = await fetch(initUrl, {
|
|
@@ -768,19 +759,45 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
|
|
|
768
759
|
},
|
|
769
760
|
body: JSON.stringify({}),
|
|
770
761
|
});
|
|
771
|
-
if (initRes.ok)
|
|
772
|
-
await initRes.text(); // consume body to release connection
|
|
762
|
+
if (initRes.ok) return { ok: true };
|
|
763
|
+
const text = await initRes.text(); // consume body to release connection
|
|
773
764
|
if (attempt < maxRetries && (initRes.status === 404 || initRes.status === 503)) {
|
|
774
765
|
await sleep(retryDelayMs);
|
|
775
766
|
try { token = await getAccessToken(); } catch (_) {}
|
|
776
767
|
continue;
|
|
777
768
|
}
|
|
778
|
-
|
|
779
|
-
break;
|
|
769
|
+
return { ok: false, error: `${initRes.status}: ${text}` };
|
|
780
770
|
}
|
|
771
|
+
return { ok: false, error: 'initializeAuth: exhausted retries' };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Enable Firebase Auth sign-in providers: Email/Password, Anonymous and Google.
|
|
776
|
+
* Uses the Identity Toolkit Admin v2 REST API with gcloud credentials.
|
|
777
|
+
*
|
|
778
|
+
* Flow:
|
|
779
|
+
* 1. Call identityPlatform:initializeAuth to create the auth config for the project
|
|
780
|
+
* (required for new projects — PATCH fails with CONFIGURATION_NOT_FOUND without it).
|
|
781
|
+
* 2. PATCH /config to enable Email/Password and Anonymous.
|
|
782
|
+
* 3. POST defaultSupportedIdpConfigs to enable Google Sign-In.
|
|
783
|
+
* Google Sign-In requires an OAuth client_id that Firebase creates when the user
|
|
784
|
+
* enables it in the Console — if the client doesn't exist yet this step is skipped
|
|
785
|
+
* and googleSignInSkipped=true is returned so the caller can show a targeted hint.
|
|
786
|
+
*
|
|
787
|
+
* Non-fatal: returns { ok: false, error } without throwing if the API call fails.
|
|
788
|
+
*
|
|
789
|
+
* @param {string} projectId
|
|
790
|
+
* @returns {{ ok: boolean, googleSignInSkipped?: boolean, error?: string }}
|
|
791
|
+
*/
|
|
792
|
+
async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 15000 } = {}) {
|
|
793
|
+
// Step 1: Initialize Firebase Auth (non-fatal — the PATCH below still runs in
|
|
794
|
+
// case auth was already initialized). The shared helper keeps the init logic in
|
|
795
|
+
// one place so the Supabase flow can reuse it before its `deploy --only auth`.
|
|
796
|
+
await ensureFirebaseAuthInitialized(projectId, { maxRetries, retryDelayMs });
|
|
781
797
|
|
|
782
798
|
// Step 2: Enable Email/Password and Anonymous auth providers.
|
|
783
799
|
const configUrl = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config?updateMask=signIn.email,signIn.anonymous`;
|
|
800
|
+
let token;
|
|
784
801
|
let lastError;
|
|
785
802
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
786
803
|
try { token = await getAccessToken(); } catch (_) {
|
|
@@ -995,27 +1012,34 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
995
1012
|
const addResult = await addFirebaseToProject(projectId, { onProgress });
|
|
996
1013
|
if (!addResult.ok) return { ok: false, error: `[addFirebase] ${addResult.error}`, projectId };
|
|
997
1014
|
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1015
|
+
// Firebase Auth providers (Email/Password, Anonymous, Google, Apple) only matter
|
|
1016
|
+
// for the Firebase backend, whose app authenticates against Firebase Auth. In
|
|
1017
|
+
// fcmOnly mode the app is Supabase/API and authenticates elsewhere, so we leave
|
|
1018
|
+
// Firebase Auth untouched — this project exists purely for FCM/push (and, for
|
|
1019
|
+
// Supabase, the Google OAuth client, which new.js creates separately via
|
|
1020
|
+
// `firebase deploy --only auth`). Non-fatal — setup continues even if it fails.
|
|
1021
|
+
let authResult = null;
|
|
1022
|
+
if (!fcmOnly) {
|
|
1023
|
+
authResult = await enableAuthProviders(projectId);
|
|
1024
|
+
if (!authResult.ok) {
|
|
1025
|
+
onProgress('auth-providers-warn', {
|
|
1026
|
+
error: authResult.error,
|
|
1027
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
1028
|
+
});
|
|
1029
|
+
} else if (authResult.googleSignInSkipped) {
|
|
1030
|
+
// Email/Password and Anonymous were enabled. Google Sign-In needs an OAuth client
|
|
1031
|
+
// that Firebase creates when you enable it in the Console for the first time.
|
|
1032
|
+
onProgress('auth-google-warn', {
|
|
1033
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
// Providers were enabled but localhost couldn't be authorized — warn so the user
|
|
1037
|
+
// isn't surprised by [firebase_auth/unauthorized-domain] on web sign-in.
|
|
1038
|
+
if (authResult.ok && authResult.localhostAuthorized === false) {
|
|
1039
|
+
onProgress('auth-localhost-warn', {
|
|
1040
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/settings`,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1019
1043
|
}
|
|
1020
1044
|
|
|
1021
1045
|
// Firestore + Storage are Firebase-backend features (and need Blaze). In
|
|
@@ -1088,8 +1112,8 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
1088
1112
|
return {
|
|
1089
1113
|
ok: true,
|
|
1090
1114
|
projectId,
|
|
1091
|
-
authEnabled: authResult.ok,
|
|
1092
|
-
googleSignInSkipped: authResult.ok && !!authResult.googleSignInSkipped,
|
|
1115
|
+
authEnabled: authResult ? authResult.ok : null,
|
|
1116
|
+
googleSignInSkipped: authResult ? (authResult.ok && !!authResult.googleSignInSkipped) : false,
|
|
1093
1117
|
sha1Skipped,
|
|
1094
1118
|
sha1Error,
|
|
1095
1119
|
sha1ManualUrl: `https://console.firebase.google.com/project/${projectId}/settings/general/android:${bundleId}`,
|
|
@@ -1237,6 +1261,7 @@ module.exports = {
|
|
|
1237
1261
|
applyStorageCors,
|
|
1238
1262
|
checkBillingEnabled,
|
|
1239
1263
|
enableAuthProviders,
|
|
1264
|
+
ensureFirebaseAuthInitialized,
|
|
1240
1265
|
ensureLocalhostAuthorizedDomains,
|
|
1241
1266
|
listBillingAccounts,
|
|
1242
1267
|
listGcpOrganizations,
|
|
@@ -13,16 +13,29 @@ const { exec, execFile } = require('node:child_process');
|
|
|
13
13
|
const { promisify } = require('node:util');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const fs = require('fs-extra');
|
|
16
|
+
const { augmentedEnv } = require('../../../utils/env-tools');
|
|
16
17
|
|
|
17
18
|
const execAsync = promisify(exec);
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parse JSON from a CLI's stdout tolerantly. The Supabase CLI sometimes prints
|
|
23
|
+
* an "update available" notice before the JSON payload (more often on Windows),
|
|
24
|
+
* which would break a strict JSON.parse — so we grab from the first [ or {.
|
|
25
|
+
*/
|
|
26
|
+
function parseJsonLoose(stdout) {
|
|
27
|
+
const s = (stdout || '').trim();
|
|
28
|
+
const start = s.search(/[[{]/);
|
|
29
|
+
if (start === -1) return null;
|
|
30
|
+
try { return JSON.parse(s.slice(start)); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
async function run(cmd, cwd, env = {}) {
|
|
21
34
|
try {
|
|
22
35
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
23
36
|
cwd,
|
|
24
37
|
maxBuffer: 10 * 1024 * 1024,
|
|
25
|
-
env: { ...
|
|
38
|
+
env: { ...augmentedEnv(), ...env },
|
|
26
39
|
});
|
|
27
40
|
return { ok: true, stdout, stderr };
|
|
28
41
|
} catch (err) {
|
|
@@ -40,13 +53,14 @@ async function run(cmd, cwd, env = {}) {
|
|
|
40
53
|
*/
|
|
41
54
|
async function checkLoggedIn() {
|
|
42
55
|
const result = await run('supabase projects list -o json', process.cwd());
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} catch {
|
|
56
|
+
// Trust the exit code: a successful command means we're authenticated. Only a
|
|
57
|
+
// non-zero exit (e.g. "Access token not provided") means login is required.
|
|
58
|
+
// The list itself is parsed best-effort (tolerant of update notices).
|
|
59
|
+
if (!result.ok) {
|
|
48
60
|
return { ok: false, error: 'Supabase login required. Run: supabase login' };
|
|
49
61
|
}
|
|
62
|
+
const data = parseJsonLoose(result.stdout);
|
|
63
|
+
return { ok: true, projects: Array.isArray(data) ? data : [] };
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -69,13 +83,9 @@ async function getProjectsByOrg(orgId) {
|
|
|
69
83
|
async function getOrgsList() {
|
|
70
84
|
const result = await run('supabase orgs list -o json', process.cwd());
|
|
71
85
|
if (!result.ok) return { ok: false, error: result.error };
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return { ok: true, orgs: orgs.map((o) => ({ id: o.id, name: o.name || o.id })) };
|
|
76
|
-
} catch {
|
|
77
|
-
return { ok: false, error: 'Could not parse orgs list. Run: supabase login' };
|
|
78
|
-
}
|
|
86
|
+
const data = parseJsonLoose(result.stdout);
|
|
87
|
+
const orgs = Array.isArray(data) ? data : [];
|
|
88
|
+
return { ok: true, orgs: orgs.map((o) => ({ id: o.id, name: o.name || o.id })) };
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
package/lib/utils/env-tools.js
CHANGED
|
@@ -69,6 +69,30 @@ function pubCacheBin(name) {
|
|
|
69
69
|
return `"${file}"`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Quoted path to `keytool` (used for the Android debug SHA-1). It ships with
|
|
74
|
+
* the JDK and is almost never on PATH on Windows, so we look in JAVA_HOME and
|
|
75
|
+
* Android Studio's bundled JBR before falling back to bare `keytool`.
|
|
76
|
+
*/
|
|
77
|
+
function keytoolBin() {
|
|
78
|
+
const exe = isWindows ? 'keytool.exe' : 'keytool';
|
|
79
|
+
const candidates = [];
|
|
80
|
+
if (process.env.JAVA_HOME) candidates.push(path.join(process.env.JAVA_HOME, 'bin', exe));
|
|
81
|
+
if (isWindows) {
|
|
82
|
+
const pf = process.env.ProgramFiles;
|
|
83
|
+
const local = process.env.LOCALAPPDATA;
|
|
84
|
+
if (pf) candidates.push(path.win32.join(pf, 'Android', 'Android Studio', 'jbr', 'bin', exe));
|
|
85
|
+
if (local) candidates.push(path.win32.join(local, 'Programs', 'Android Studio', 'jbr', 'bin', exe));
|
|
86
|
+
if (pf) candidates.push(path.win32.join(pf, 'Android', 'Android Studio', 'jre', 'bin', exe));
|
|
87
|
+
} else {
|
|
88
|
+
candidates.push('/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/keytool');
|
|
89
|
+
}
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
if (fs.existsSync(candidate)) return `"${candidate}"`;
|
|
92
|
+
}
|
|
93
|
+
return 'keytool';
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/**
|
|
73
97
|
* Well-known install dirs of tools that may not be on the frozen session PATH
|
|
74
98
|
* yet — e.g. the Google Cloud SDK right after it's installed mid-run. Only
|
|
@@ -114,5 +138,6 @@ module.exports = {
|
|
|
114
138
|
homeDir,
|
|
115
139
|
pubCacheBinDir,
|
|
116
140
|
pubCacheBin,
|
|
141
|
+
keytoolBin,
|
|
117
142
|
augmentedEnv,
|
|
118
143
|
};
|