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
|
@@ -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);
|