create-nextblock 0.9.6 → 0.9.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.9.6",
3
+ "version": "0.9.61",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -32,6 +32,36 @@ export interface ConnectionInput {
32
32
  accessToken?: string;
33
33
  }
34
34
 
35
+ /**
36
+ * Sanity-check a Supabase API key offline. Legacy keys are JWTs carrying { role, ref };
37
+ * newer keys are opaque sb_secret_* / sb_publishable_* strings. We use this to reject an
38
+ * anon key pasted into the service-role field (and vice-versa) BEFORE it gets written —
39
+ * otherwise it only surfaces much later as "permission denied" on the first write, since
40
+ * a SELECT probe can't tell the keys apart (anon can also read site_settings).
41
+ */
42
+ function inspectSupabaseKey(key: string): {
43
+ role?: string;
44
+ ref?: string;
45
+ format: 'jwt' | 'secret' | 'publishable' | 'unknown';
46
+ } {
47
+ if (key.startsWith('sb_secret_')) return { role: 'service_role', format: 'secret' };
48
+ if (key.startsWith('sb_publishable_')) return { role: 'anon', format: 'publishable' };
49
+ const parts = key.split('.');
50
+ if (parts.length === 3) {
51
+ try {
52
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
53
+ return {
54
+ role: typeof payload?.role === 'string' ? payload.role : undefined,
55
+ ref: typeof payload?.ref === 'string' ? payload.ref : undefined,
56
+ format: 'jwt',
57
+ };
58
+ } catch {
59
+ // not a decodable JWT — fall through to 'unknown'
60
+ }
61
+ }
62
+ return { format: 'unknown' };
63
+ }
64
+
35
65
  /**
36
66
  * Step (Profile B / local only): validate the Supabase credentials, then persist them
37
67
  * to `.env.local` and the live process. Probes with the service-role key so we can
@@ -56,6 +86,38 @@ export async function saveSupabaseConnection(input: ConnectionInput): Promise<Ac
56
86
  return { ok: false, error: 'The Supabase URL is not a valid URL.' };
57
87
  }
58
88
 
89
+ // The anon and service-role keys are easy to swap (both start with "eyJ"). Catch a
90
+ // swapped or wrong-project key here, offline, with a clear message.
91
+ const svcKey = inspectSupabaseKey(serviceRoleKey);
92
+ if (svcKey.role && svcKey.role !== 'service_role') {
93
+ return {
94
+ ok: false,
95
+ error: `That service-role key is actually the "${svcKey.role}" key. Paste the secret service_role key from Supabase → Project Settings → API.`,
96
+ };
97
+ }
98
+ const anonKeyInfo = inspectSupabaseKey(anonKey);
99
+ if (anonKeyInfo.role && anonKeyInfo.role !== 'anon') {
100
+ return {
101
+ ok: false,
102
+ error: `That anon key is actually the "${anonKeyInfo.role}" key. Paste the public anon key from Supabase → Project Settings → API.`,
103
+ };
104
+ }
105
+ let urlRef: string | undefined;
106
+ try {
107
+ const host = new URL(supabaseUrl).hostname;
108
+ if (host.endsWith('.supabase.co') || host.endsWith('.supabase.in')) {
109
+ urlRef = host.split('.')[0];
110
+ }
111
+ } catch {
112
+ // already validated above
113
+ }
114
+ if (urlRef && svcKey.ref && svcKey.ref !== urlRef) {
115
+ return {
116
+ ok: false,
117
+ error: `That service-role key belongs to project "${svcKey.ref}", but the URL is project "${urlRef}". Use keys from the same project.`,
118
+ };
119
+ }
120
+
59
121
  if (!isLocalWritableEnv()) {
60
122
  return {
61
123
  ok: false,
@@ -69,6 +131,27 @@ export async function saveSupabaseConnection(input: ConnectionInput): Promise<Ac
69
131
  auth: { persistSession: false, autoRefreshToken: false },
70
132
  });
71
133
 
134
+ // Definitive service-role check: only the service_role can call the GoTrue admin API.
135
+ // A SELECT on site_settings can't tell service_role from anon (both can read it), so
136
+ // this catches a rotated/invalid key that the offline inspection above can't. Works on
137
+ // a fresh project too (the auth schema always exists, independent of the public schema).
138
+ try {
139
+ const { error: adminErr } = await probe.auth.admin.listUsers({ page: 1, perPage: 1 });
140
+ if (adminErr) {
141
+ return {
142
+ ok: false,
143
+ error: `That key can't perform admin actions (${adminErr.message}). Confirm you pasted the secret service_role key from Supabase → Project Settings → API.`,
144
+ };
145
+ }
146
+ } catch (caught) {
147
+ return {
148
+ ok: false,
149
+ error: `Could not verify the service-role key: ${
150
+ caught instanceof Error ? caught.message : 'unknown error'
151
+ }`,
152
+ };
153
+ }
154
+
72
155
  let schemaReady = false;
73
156
  try {
74
157
  const { error } = await probe.from('site_settings').select('key').limit(1);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.9.6",
3
+ "version": "0.9.61",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",